mcpField
S
supabase-performance-tuning

supabase-performance-tuning

github.com/jeremylongshore数据与数据库2.9k47 次安装2 天前更新
代码仓库

关于 supabase-performance-tuning

supabase-performance-tuning 是面向 Supabase 项目的性能调优技能,涵盖 PostgreSQL、Supabase 基础设施和客户端 SDK。通过诊断和执行计划比较来定位瓶颈并验证优化效果。

主要功能

  1. 01诊断慢查询与数据库指标
  2. 02分析执行计划并创建索引
  3. 03优化客户端查询与分页
  4. 04使用 RPC 和物化视图
  5. 05配置连接池与读取副本
  6. 06将计算任务部署到 Edge Functions

适用场景

  1. 01排查慢查询和索引使用问题
  2. 02减少客户端查询数据量
  3. 03加速重复的聚合计算
  4. 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.md
errors.md
examples.md

详细介绍

Supabase 性能调优

概述

从三个层面系统地提升 Supabase 查询和数据库性能:PostgreSQL 引擎(索引、查询计划、物化视图)、Supabase 基础设施(Supavisor 连接池、Edge Functions、只读副本)以及客户端 SDK 模式(列选择、分页、RPC 函数)。这里的每项技术都可以衡量——前后运行 EXPLAIN ANALYZE,确认改进效果。

前提条件

  • Supabase 项目(本地或托管),已安装 @supabase/supabase-js v2+
  • 已安装 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
响应载荷超过 1MBselect('*') 返回了所有列/行使用 .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)

资源

后续步骤

  • 有关 RLS 策略设计,请参阅 supabase-rls-policies
  • 有关成本优化,请参阅 supabase-cost-tuning
  • 有关实时订阅,请参阅 supabase-realtime

评论

支持 Markdown