mcpField
S
supabase-performance-tuning

supabase-performance-tuning

github.com/jeremylongshoreデータ・データベース2.9k47 回インストール2 日前に更新
リポジトリ

supabase-performance-tuning について

supabase-performance-tuning は、PostgreSQL、インフラ、クライアント SDK の各面から Supabase プロジェクトを改善するスキルです。診断とクエリプランの比較でボトルネックを特定し、変更の効果を確認します。

主な機能

  1. 01遅いクエリと DB 指標の診断
  2. 02クエリプランの分析とインデックス作成
  3. 03クライアントクエリとページネーションの最適化
  4. 04RPC 関数とマテリアライズドビューの活用
  5. 05プーリングとリードレプリカの設定
  6. 06Edge 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 のパフォーマンスチューニング

概要

3 つのレイヤーにわたって、Supabase のクエリとデータベースのパフォーマンスを体系的に向上させます。PostgreSQL エンジン(インデックス、クエリプラン、マテリアライズドビュー)、Supabase インフラストラクチャ(Supavisor のコネクションプーリング、Edge Functions、リードレプリカ)、クライアント SDK のパターン(カラム選択、ページネーション、RPC 関数)です。ここで紹介する手法はすべて測定可能です。改善を確認するため、実行前と実行後に EXPLAIN ANALYZE を実行してください。

前提条件

  • @supabase/supabase-js v2+ がインストールされた Supabase プロジェクト(ローカルまたはホステッド)
  • Supabase CLI がインストールされていること(確認するには npx supabase --version を実行)
  • Supabase ダッシュボードの 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)

複雑なロジックを Postgres に移すには、RPC 関数を使用します:

-- 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 で検証され、Seq Scan ではなく Index 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 関数にリファクタリングするか、IN の代わりに EXISTS を使います
レスポンスペイロードが 1MB を超えるselect('*') がすべての列と行を返している.select('col1, col2') と .range() を使ってページネーションします
マテリアライズドビューのデータが古い書き込み後にビューが更新されていないREFRESH MATERIALIZED VIEW CONCURRENTLY を実行するよう、pg_cron または cron Edge Function を設定します
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 を参照してください

コメント