关于 supabase-performance-tuning
supabase-performance-tuning 是面向 Supabase 项目的性能调优技能,涵盖 PostgreSQL、Supabase 基础设施和客户端 SDK。通过诊断和执行计划比较来定位瓶颈并验证优化效果。
主要功能
- 01诊断慢查询与数据库指标
- 02分析执行计划并创建索引
- 03优化客户端查询与分页
- 04使用 RPC 和物化视图
- 05配置连接池与读取副本
- 06将计算任务部署到 Edge Functions
适用场景
- 01排查慢查询和索引使用问题
- 02减少客户端查询数据量
- 03加速重复的聚合计算
- 04缓解服务端应用的连接池压力
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jeremylongshore/ tons-of-skills-marketplace --skill supabase-performance-tuning安装后在 Claude Code 中输入/supabase-performance-tuning确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/caching-strategy.mderrors.mdexamples.md详细介绍
Supabase 性能调优
概述
从三个层面系统地提升 Supabase 查询和数据库性能:PostgreSQL 引擎(索引、查询计划、物化视图)、Supabase 基础设施(Supavisor 连接池、Edge Functions、只读副本)以及客户端 SDK 模式(列选择、分页、RPC 函数)。这里的每项技术都可以衡量——前后运行 EXPLAIN ANALYZE,确认改进效果。
前提条件
- Supabase 项目(本地或托管),已安装
@supabase/supabase-jsv2+ - 已安装 Supabase CLI(运行
npx supabase --version验证) - 可访问 Supabase Dashboard 中的 SQL Editor,或可直连 Postgres
- 已启用
pg_stat_statements扩展(步骤 1 会介绍如何启用)
操作说明
步骤 1:诊断 — 找出性能瓶颈
每次性能优化都应从数据入手。启用 pg_stat_statements 并运行 Supabase CLI 诊断,以便在优化前找出瓶颈。
启用统计扩展:
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
按平均执行时间找出最慢的查询:
SELECT
query,
calls,
mean_exec_time::numeric(10,2) AS avg_ms,
total_exec_time::numeric(10,2) AS total_ms,
rows
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
使用 Supabase CLI 检查索引使用情况和缓存命中率:
# Which indexes are actually being used?
npx supabase inspect db index-usage
# What percentage of queries are served from cache vs disk?
npx supabase inspect db cache-hit
# Tables consuming the most space
npx supabase inspect db table-sizes
检查活动连接,排查连接池问题:
SELECT state, count(*), max(age(now(), state_change)) AS max_age
FROM pg_stat_activity
WHERE datname = current_database()
GROUP BY state;
如果 idle 连接数超过套餐限制,或 active 查询显示较高的 max_age,应优先处理连接池(步骤 2)和查询优化(步骤 3)。
步骤 2:索引和查询计划
索引是效果最显著的优化手段。使用 EXPLAIN ANALYZE 查看查询计划,然后创建有针对性的索引。
查看查询计划:
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM users WHERE email = '[email protected]';
在大型表上查找 Seq Scan——这表示没有使用索引。添加索引后,计划应显示 Index Scan 或 Index Only Scan。
创建基本索引:
CREATE INDEX idx_users_email ON users(email);
为多列筛选条件创建复合索引:
-- Optimizes: WHERE user_id = ? AND created_at > ? ORDER BY created_at DESC
CREATE INDEX idx_orders_user_created
ON orders(user_id, created_at DESC);
为常见筛选模式创建部分索引:
-- Only indexes incomplete todos — much smaller and faster than full-table index
CREATE INDEX idx_todos_user_incomplete
ON todos(user_id, inserted_at DESC)
WHERE is_complete = false;
查找外键上缺失的索引(JOIN 变慢的常见原因):
SELECT
tc.table_name,
kcu.column_name AS fk_column,
'CREATE INDEX idx_' || tc.table_name || '_' || kcu.column_name
|| ' ON public.' || tc.table_name || '(' || kcu.column_name || ');' AS fix
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
LEFT JOIN pg_indexes i
ON i.tablename = tc.table_name
AND i.indexdef LIKE '%' || kcu.column_name || '%'
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema = 'public'
AND i.indexname IS NULL;
查找未使用的索引(可考虑删除以减少写入开销):
SELECT schemaname, relname, indexrelname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0 AND schemaname = 'public'
ORDER BY pg_relation_size(indexrelid) DESC;
在生产表上始终使用 CREATE INDEX CONCURRENTLY,避免创建索引时锁定写入操作。
步骤 3:客户端 SDK 和基础设施优化
先优化 Supabase JS 客户端调用,再利用基础设施功能扩展规模。
只选择所需的列——避免使用 select('*'):
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!
)
// BAD: fetches every column, large payloads
const { data } = await supabase.from('users').select('*')
// GOOD: only the columns you need
const { data } = await supabase.from('users').select('id, name, avatar_url')
使用 .range() 分页,避免一次加载所有行:
// Page 1: rows 0-49
const { data: page1 } = await supabase
.from('products')
.select('id, name, price')
.order('created_at', { ascending: false })
.range(0, 49)
// Page 2: rows 50-99
const { data: page2 } = await supabase
.from('products')
.select('id, name, price')
.order('created_at', { ascending: false })
.range(50, 99)
使用 RPC 函数将复杂逻辑交由 Postgres 处理:
-- Create a server-side function for an expensive aggregation
CREATE OR REPLACE FUNCTION get_dashboard_stats(org_id uuid)
RETURNS json AS $$
SELECT json_build_object(
'total_users', (SELECT count(*) FROM users WHERE organization_id = org_id),
'active_projects', (SELECT count(*) FROM projects WHERE organization_id = org_id AND status = 'active'),
'tasks_completed_30d', (SELECT count(*) FROM tasks t
JOIN projects p ON p.id = t.project_id
WHERE p.organization_id = org_id
AND t.completed_at > now() - interval '30 days')
);
$$ LANGUAGE sql STABLE;
// One network call instead of three separate queries
const { data } = await supabase.rpc('get_dashboard_stats', {
org_id: 'your-org-uuid'
})
为开销较大的聚合创建物化视图:
-- Precompute a leaderboard instead of recalculating on every request
CREATE MATERIALIZED VIEW leaderboard AS
SELECT
u.id,
u.username,
count(t.id) AS tasks_completed,
rank() OVER (ORDER BY count(t.id) DESC) AS rank
FROM users u
LEFT JOIN tasks t ON t.assignee_id = u.id AND t.status = 'done'
GROUP BY u.id, u.username;
-- Create an index on the materialized view
CREATE UNIQUE INDEX idx_leaderboard_user ON leaderboard(id);
-- Refresh on a schedule (e.g., via pg_cron or a cron Edge Function)
REFRESH MATERIALIZED VIEW CONCURRENTLY leaderboard;
使用 Supavisor 配置连接池:
// For serverless environments (Vercel, Netlify, Cloudflare Workers):
// Use the pooled connection string with transaction mode
// Dashboard → Settings → Database → Connection string → "Transaction mode"
// The JS SDK uses PostgREST (HTTP) which has its own pooling — no config needed.
// Direct Postgres clients (Prisma, Drizzle, pg) need the pooled string:
import { Pool } from 'pg'
const pool = new Pool({
connectionString: process.env.SUPABASE_DATABASE_URL,
max: 5, // Keep low in serverless — Supavisor manages the upstream pool
idleTimeoutMillis: 10000,
})
使用 Edge Functions 在靠近数据的位置执行计算密集型操作:
// supabase/functions/generate-report/index.ts
// Edge Functions run in the same region as your database — low latency
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
Deno.serve(async (req) => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
// Heavy aggregation runs next to the database, not in the user's browser
const { data } = await supabase.rpc('get_dashboard_stats', {
org_id: (await req.json()).org_id
})
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' }
})
})
在 Pro+ 套餐上启用只读副本,以应对读取密集型工作负载——将分析和报表查询路由到副本,减轻主库负载。
输出
完成这些步骤后,你将获得:
- 基于
pg_stat_statements、index-usage和cache-hit的诊断基线 - 针对慢查询列、外键和常见筛选模式的索引
- 经
EXPLAIN ANALYZE验证的查询计划,其中显示Index Scan而非Seq Scan - 通过列选择、分页和联表查询优化的客户端查询
- 用于开销较大的服务端聚合的 RPC 函数和物化视图
- 通过 Supavisor 为无服务器部署配置的连接池
- 在数据库附近部署、用于计算密集型操作的 Edge Functions
错误处理
| 症状 | 原因 | 修复方法 |
|---|---|---|
大型表的 EXPLAIN 输出中出现 Seq Scan | 筛选或排序列缺少索引 | 在 WHERE/ORDER BY 子句中的列上执行 CREATE INDEX |
PGRST000: could not connect to server | 连接池耗尽 | 切换到 Supavisor 池化连接字符串;在无服务器环境中减小 max 池大小 |
RLS 策略较慢(可在 pg_stat_statements 中看到) | 策略中的子查询会逐行执行 | 重构为 security definer 函数,或使用 EXISTS 替代 IN |
| 响应载荷超过 1MB | select('*') 返回了所有列/行 | 使用 .select('col1, col2') 并通过 .range() 分页 |
| 物化视图数据过时 | 写入后未刷新视图 | 设置 pg_cron 或 cron Edge Function 来运行 REFRESH MATERIALIZED VIEW CONCURRENTLY |
cache-hit 比率低于 99% | 工作集超出 RAM(shared_buffers) | 升级计算附加组件,或优化查询以减少访问的页面 |
| 聚合端点延迟较高 | 每次请求都实时计算聚合结果 | 改用物化视图或 RPC 函数;在 Edge Function 层缓存 |
示例
索引优化前后:
-- Before: 450ms, Seq Scan
EXPLAIN (ANALYZE) SELECT * FROM orders WHERE customer_id = 'abc-123';
-- Seq Scan on orders (cost=0.00..15234.00 rows=50 width=128) (actual time=0.015..450.123 rows=50 loops=1)
CREATE INDEX idx_orders_customer ON orders(customer_id);
-- After: 0.8ms, Index Scan
EXPLAIN (ANALYZE) SELECT * FROM orders WHERE customer_id = 'abc-123';
-- Index Scan using idx_orders_customer on orders (cost=0.42..8.44 rows=50 width=128) (actual time=0.025..0.812 rows=50 loops=1)
客户端查询优化——消除 N+1:
// BAD: N+1 — one query per project (10 projects = 11 queries)
const { data: projects } = await supabase.from('projects').select('id, name')
for (const project of projects!) {
const { data: tasks } = await supabase
.from('tasks').select('*').eq('project_id', project.id)
}
// GOOD: Single query with embedded join (1 query total)
const { data } = await supabase
.from('projects')
.select('id, name, tasks(id, title, status)')
.eq('organization_id', orgId)
资源
- Supabase 性能顾问 — 内置 CLI 诊断工具
- PostgreSQL 索引类型 — B-tree、GIN、GiST,以及各自的适用场景
- 使用 Supavisor 进行连接池管理 — 事务模式与会话模式
- Supabase Edge Functions — 在数据库旁部署无服务器函数
- 只读副本 — 在 Pro+ 计划中分担读取密集型查询
- RLS 性能最佳实践 — 避免逐行执行子查询
后续步骤
- 有关 RLS 策略设计,请参阅
supabase-rls-policies - 有关成本优化,请参阅
supabase-cost-tuning - 有关实时订阅,请参阅
supabase-realtime