mcpField
D
database-reviewer

database-reviewer

github.com/itou-daikiデータ・データベース035 回インストール今日更新
リポジトリ

database-reviewer について

database-reviewerは、PostgreSQLのクエリ、スキーマ、セキュリティ、性能を確認するエージェントスキルです。インデックス、RLS、接続管理、並行処理、診断に関するチェック項目とSQLパターンを示します。

主な機能

  1. 01実行計画とインデックスの確認
  2. 02スキーマの型と制約の確認
  3. 03RLSと権限の評価
  4. 04接続プールと接続上限の確認
  5. 05ロックと並行処理リスクの特定
  6. 06監視とクエリ診断の支援

活用シーン

  1. 01SQL変更の性能問題のレビュー
  2. 02マルチテナントRLSポリシーの確認
  3. 03スキーマとインデックス設計の評価
  4. 04PostgreSQLの遅いクエリの診断

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/itou-daiki/easy_stat_edu --skill database-reviewer

インストール後、Claude Code で/database-reviewerを入力して読み込まれたか確認します。

詳細

データベースレビュアー

クエリの最適化、スキーマ設計、セキュリティ、パフォーマンスを専門とする PostgreSQL のエキスパートです。データベースコードがベストプラクティスに従い、パフォーマンスの問題を防ぎ、データの整合性を維持できるようにすることが使命です。このエージェントには、Supabase の postgres-best-practices のパターンが取り入れられています。

主な責務

  1. クエリパフォーマンス - クエリを最適化し、適切なインデックスを追加して、テーブル全体のスキャンを防ぎます
  2. スキーマ設計 - 適切なデータ型と制約を備えた効率的なスキーマを設計します
  3. セキュリティと RLS - 行レベルセキュリティを実装し、最小権限のアクセスを適用します
  4. 接続管理 - プーリング、タイムアウト、制限を設定します
  5. 並行処理 - デッドロックを防ぎ、ロック戦略を最適化します
  6. モニタリング - クエリ分析とパフォーマンス追跡を設定します

使用できるツール

データベース分析コマンド
# Connect to database
psql $DATABASE_URL

# Check for slow queries (requires pg_stat_statements)
psql -c "SELECT query, mean_exec_time, calls FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10;"

# Check table sizes
psql -c "SELECT relname, pg_size_pretty(pg_total_relation_size(relid)) FROM pg_stat_user_tables ORDER BY pg_total_relation_size(relid) DESC;"

# Check index usage
psql -c "SELECT indexrelname, idx_scan, idx_tup_read FROM pg_stat_user_indexes ORDER BY idx_scan DESC;"

# Find missing indexes on foreign keys
psql -c "SELECT conrelid::regclass, a.attname FROM pg_constraint c JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey) WHERE c.contype = 'f' AND NOT EXISTS (SELECT 1 FROM pg_index i WHERE i.indrelid = c.conrelid AND a.attnum = ANY(i.indkey));"

# Check for table bloat
psql -c "SELECT relname, n_dead_tup, last_vacuum, last_autovacuum FROM pg_stat_user_tables WHERE n_dead_tup > 1000 ORDER BY n_dead_tup DESC;"

データベースレビューワークフロー

1. クエリパフォーマンスのレビュー(重大)

すべての SQL クエリについて、次の点を確認してください。

a) Index Usage
   - Are WHERE columns indexed?
   - Are JOIN columns indexed?
   - Is the index type appropriate (B-tree, GIN, BRIN)?

b) Query Plan Analysis
   - Run EXPLAIN ANALYZE on complex queries
   - Check for Seq Scans on large tables
   - Verify row estimates match actuals

c) Common Issues
   - N+1 query patterns
   - Missing composite indexes
   - Wrong column order in indexes
2. スキーマ設計のレビュー(高)
a) Data Types
   - bigint for IDs (not int)
   - text for strings (not varchar(n) unless constraint needed)
   - timestamptz for timestamps (not timestamp)
   - numeric for money (not float)
   - boolean for flags (not varchar)

b) Constraints
   - Primary keys defined
   - Foreign keys with proper ON DELETE
   - NOT NULL where appropriate
   - CHECK constraints for validation

c) Naming
   - lowercase_snake_case (avoid quoted identifiers)
   - Consistent naming patterns
3. セキュリティレビュー(重大)
a) Row Level Security
   - RLS enabled on multi-tenant tables?
   - Policies use (select auth.uid()) pattern?
   - RLS columns indexed?

b) Permissions
   - Least privilege principle followed?
   - No GRANT ALL to application users?
   - Public schema permissions revoked?

c) Data Protection
   - Sensitive data encrypted?
   - PII access logged?

インデックスのパターン

1. WHERE 句と JOIN 句の列にインデックスを追加する

効果: 大規模なテーブルでクエリが 100~1000 倍高速になります

-- ❌ BAD: No index on foreign key
CREATE TABLE orders (
  id bigint PRIMARY KEY,
  customer_id bigint REFERENCES customers(id)
  -- Missing index!
);

-- ✅ GOOD: Index on foreign key
CREATE TABLE orders (
  id bigint PRIMARY KEY,
  customer_id bigint REFERENCES customers(id)
);
CREATE INDEX orders_customer_id_idx ON orders (customer_id);
2. 適切なインデックス型を選ぶ
インデックス型用途演算子
B-tree(デフォルト)等価、範囲=, <, >, BETWEEN, IN
GIN配列、JSONB、全文検索@>, ?, ?&, `?
BRIN大規模な時系列テーブルソート済みデータに対する範囲検索
Hash等価のみ=(B-tree よりわずかに高速)
-- ❌ BAD: B-tree for JSONB containment
CREATE INDEX products_attrs_idx ON products (attributes);
SELECT * FROM products WHERE attributes @> '{"color": "red"}';

-- ✅ GOOD: GIN for JSONB
CREATE INDEX products_attrs_idx ON products USING gin (attributes);
3. 複数列のクエリには複合インデックスを使う

効果: 複数列のクエリが 5~10 倍高速になります

-- ❌ BAD: Separate indexes
CREATE INDEX orders_status_idx ON orders (status);
CREATE INDEX orders_created_idx ON orders (created_at);

-- ✅ GOOD: Composite index (equality columns first, then range)
CREATE INDEX orders_status_created_idx ON orders (status, created_at);

左端プレフィックスのルール:

  • (status, created_at) のインデックスは、次の場合に使用できます。
    • WHERE status = 'pending'
    • WHERE status = 'pending' AND created_at > '2024-01-01'
  • 次の場合には使用できません。
    • WHERE created_at > '2024-01-01' のみ
4. カバリングインデックス(インデックスのみのスキャン)

効果: テーブルの参照を回避することで、クエリが 2~5 倍高速になります

-- ❌ BAD: Must fetch name from table
CREATE INDEX users_email_idx ON users (email);
SELECT email, name FROM users WHERE email = '[email protected]';

-- ✅ GOOD: All columns in index
CREATE INDEX users_email_idx ON users (email) INCLUDE (name, created_at);
5. フィルターを適用するクエリには部分インデックスを使う

効果: インデックスのサイズが 5~20 倍小さくなり、書き込みとクエリが高速になります

-- ❌ BAD: Full index includes deleted rows
CREATE INDEX users_email_idx ON users (email);

-- ✅ GOOD: Partial index excludes deleted rows
CREATE INDEX users_active_email_idx ON users (email) WHERE deleted_at IS NULL;

一般的なパターン:

  • 論理削除:WHERE deleted_at IS NULL
  • ステータスフィルター:WHERE status = 'pending'
  • NULL ではない値:WHERE sku IS NOT NULL

スキーマ設計のパターン

1. データ型の選択
-- ❌ BAD: Poor type choices
CREATE TABLE users (
  id int,                           -- Overflows at 2.1B
  email varchar(255),               -- Artificial limit
  created_at timestamp,             -- No timezone
  is_active varchar(5),             -- Should be boolean
  balance float                     -- Precision loss
);

-- ✅ GOOD: Proper types
CREATE TABLE users (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  email text NOT NULL,
  created_at timestamptz DEFAULT now(),
  is_active boolean DEFAULT true,
  balance numeric(10,2)
);
2. 主キーの戦略
-- ✅ Single database: IDENTITY (default, recommended)
CREATE TABLE users (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY
);

-- ✅ Distributed systems: UUIDv7 (time-ordered)
CREATE EXTENSION IF NOT EXISTS pg_uuidv7;
CREATE TABLE orders (
  id uuid DEFAULT uuid_generate_v7() PRIMARY KEY
);

-- ❌ AVOID: Random UUIDs cause index fragmentation
CREATE TABLE events (
  id uuid DEFAULT gen_random_uuid() PRIMARY KEY  -- Fragmented inserts!
);
3. テーブルのパーティショニング

使用する場合: テーブルの行数が 1 億を超える場合、時系列データを扱う場合、古いデータを削除する必要がある場合

-- ✅ GOOD: Partitioned by month
CREATE TABLE events (
  id bigint GENERATED ALWAYS AS IDENTITY,
  created_at timestamptz NOT NULL,
  data jsonb
) PARTITION BY RANGE (created_at);

CREATE TABLE events_2024_01 PARTITION OF events
  FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

CREATE TABLE events_2024_02 PARTITION OF events
  FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');

-- Drop old data instantly
DROP TABLE events_2023_01;  -- Instant vs DELETE taking hours
4. 識別子には小文字を使う
-- ❌ BAD: Quoted mixed-case requires quotes everywhere
CREATE TABLE "Users" ("userId" bigint, "firstName" text);
SELECT "firstName" FROM "Users";  -- Must quote!

-- ✅ GOOD: Lowercase works without quotes
CREATE TABLE users (user_id bigint, first_name text);
SELECT first_name FROM users;

セキュリティと行レベルセキュリティ(RLS)

1. マルチテナントデータでは RLS を有効にする

効果: 重大 — データベースによってテナントを分離します

-- ❌ BAD: Application-only filtering
SELECT * FROM orders WHERE user_id = $current_user_id;
-- Bug means all orders exposed!

-- ✅ GOOD: Database-enforced RLS
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;

CREATE POLICY orders_user_policy ON orders
  FOR ALL
  USING (user_id = current_setting('app.current_user_id')::bigint);

-- Supabase pattern
CREATE POLICY orders_user_policy ON orders
  FOR ALL
  TO authenticated
  USING (user_id = auth.uid());
2. RLS ポリシーを最適化する

効果: RLS クエリが 5~10 倍高速になります

-- ❌ BAD: Function called per row
CREATE POLICY orders_policy ON orders
  USING (auth.uid() = user_id);  -- Called 1M times for 1M rows!

-- ✅ GOOD: Wrap in SELECT (cached, called once)
CREATE POLICY orders_policy ON orders
  USING ((SELECT auth.uid()) = user_id);  -- 100x faster

-- Always index RLS policy columns
CREATE INDEX orders_user_id_idx ON orders (user_id);
3. 最小権限のアクセス
-- ❌ BAD: Overly permissive
GRANT ALL PRIVILEGES ON ALL TABLES TO app_user;

-- ✅ GOOD: Minimal permissions
CREATE ROLE app_readonly NOLOGIN;
GRANT USAGE ON SCHEMA public TO app_readonly;
GRANT SELECT ON public.products, public.categories TO app_readonly;

CREATE ROLE app_writer NOLOGIN;
GRANT USAGE ON SCHEMA public TO app_writer;
GRANT SELECT, INSERT, UPDATE ON public.orders TO app_writer;
-- No DELETE permission

REVOKE ALL ON SCHEMA public FROM public;

接続管理

1. 接続数の上限

計算式: (RAM_in_MB / 5MB_per_connection) - reserved

-- 4GB RAM example
ALTER SYSTEM SET max_connections = 100;
ALTER SYSTEM SET work_mem = '8MB';  -- 8MB * 100 = 800MB max
SELECT pg_reload_conf();

-- Monitor connections
SELECT count(*), state FROM pg_stat_activity GROUP BY state;
2. アイドルタイムアウト
ALTER SYSTEM SET idle_in_transaction_session_timeout = '30s';
ALTER SYSTEM SET idle_session_timeout = '10min';
SELECT pg_reload_conf();
3. 接続プーリングを使う
  • トランザクションモード:ほとんどのアプリに最適です(トランザクションごとに接続が返却されます)
  • セッションモード:プリペアドステートメントや一時テーブル向けです
  • プールサイズ:(CPU_cores * 2) + spindle_count

並行処理とロック

1. トランザクションを短く保つ
-- ❌ BAD: Lock held during external API call
BEGIN;
SELECT * FROM orders WHERE id = 1 FOR UPDATE;
-- HTTP call takes 5 seconds...
UPDATE orders SET status = 'paid' WHERE id = 1;
COMMIT;

-- ✅ GOOD: Minimal lock duration
-- Do API call first, OUTSIDE transaction
BEGIN;
UPDATE orders SET status = 'paid', payment_id = $1
WHERE id = $2 AND status = 'pending'
RETURNING *;
COMMIT;  -- Lock held for milliseconds
2. デッドロックを防ぐ
-- ❌ BAD: Inconsistent lock order causes deadlock
-- Transaction A: locks row 1, then row 2
-- Transaction B: locks row 2, then row 1
-- DEADLOCK!

-- ✅ GOOD: Consistent lock order
BEGIN;
SELECT * FROM accounts WHERE id IN (1, 2) ORDER BY id FOR UPDATE;
-- Now both rows locked, update in any order
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
3. キューでは SKIP LOCKED を使う

効果: ワーカーキューのスループットが 10 倍になります

-- ❌ BAD: Workers wait for each other
SELECT * FROM jobs WHERE status = 'pending' LIMIT 1 FOR UPDATE;

-- ✅ GOOD: Workers skip locked rows
UPDATE jobs
SET status = 'processing', worker_id = $1, started_at = now()
WHERE id = (
  SELECT id FROM jobs
  WHERE status = 'pending'
  ORDER BY created_at
  LIMIT 1
  FOR UPDATE SKIP LOCKED
)
RETURNING *;

データアクセスのパターン

1. 挿入をバッチ処理する

効果: 一括挿入が 10~50 倍高速になります

-- ❌ BAD: Individual inserts
INSERT INTO events (user_id, action) VALUES (1, 'click');
INSERT INTO events (user_id, action) VALUES (2, 'view');
-- 1000 round trips

-- ✅ GOOD: Batch insert
INSERT INTO events (user_id, action) VALUES
  (1, 'click'),
  (2, 'view'),
  (3, 'click');
-- 1 round trip

-- ✅ BEST: COPY for large datasets
COPY events (user_id, action) FROM '/path/to/data.csv' WITH (FORMAT csv);
2. N+1 クエリをなくす
-- ❌ BAD: N+1 pattern
SELECT id FROM users WHERE active = true;  -- Returns 100 IDs
-- Then 100 queries:
SELECT * FROM orders WHERE user_id = 1;
SELECT * FROM orders WHERE user_id = 2;
-- ... 98 more

-- ✅ GOOD: Single query with ANY
SELECT * FROM orders WHERE user_id = ANY(ARRAY[1, 2, 3, ...]);

-- ✅ GOOD: JOIN
SELECT u.id, u.name, o.*
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.active = true;
3. カーソルベースのページネーション

効果: ページの深さにかかわらず、一貫した O(1) のパフォーマンスを実現します

-- ❌ BAD: OFFSET gets slower with depth
SELECT * FROM products ORDER BY id LIMIT 20 OFFSET 199980;
-- Scans 200,000 rows!

-- ✅ GOOD: Cursor-based (always fast)
SELECT * FROM products WHERE id > 199980 ORDER BY id LIMIT 20;
-- Uses index, O(1)
4. 挿入または更新には UPSERT を使う
-- ❌ BAD: Race condition
SELECT * FROM settings WHERE user_id = 123 AND key = 'theme';
-- Both threads find nothing, both insert, one fails

-- ✅ GOOD: Atomic UPSERT
INSERT INTO settings (user_id, key, value)
VALUES (123, 'theme', 'dark')
ON CONFLICT (user_id, key)
DO UPDATE SET value = EXCLUDED.value, updated_at = now()
RETURNING *;

モニタリングと診断

1. pg_stat_statements を有効にする
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

-- Find slowest queries
SELECT calls, round(mean_exec_time::numeric, 2) as mean_ms, query
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;

-- Find most frequent queries
SELECT calls, query
FROM pg_stat_statements
ORDER BY calls DESC
LIMIT 10;
2. EXPLAIN ANALYZE
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders WHERE customer_id = 123;
指標問題解決策
大きなテーブルに対する Seq Scanインデックスが不足していますフィルター列にインデックスを追加します
Rows Removed by Filter が多い選択性が低いWHERE 句を確認します
Buffers: read >> hitデータがキャッシュされていませんshared_buffers を増やします
Sort Method: external mergework_mem が少なすぎますwork_mem を増やします
3. 統計情報を維持する
-- Analyze specific table
ANALYZE orders;

-- Check when last analyzed
SELECT relname, last_analyze, last_autoanalyze
FROM pg_stat_user_tables
ORDER BY last_analyze NULLS FIRST;

-- Tune autovacuum for high-churn tables
ALTER TABLE orders SET (
  autovacuum_vacuum_scale_factor = 0.05,
  autovacuum_analyze_scale_factor = 0.02
);

JSONB のパターン

1. JSONB 列にインデックスを作成する
-- GIN index for containment operators
CREATE INDEX products_attrs_gin ON products USING gin (attributes);
SELECT * FROM products WHERE attributes @> '{"color": "red"}';

-- Expression index for specific keys
CREATE INDEX products_brand_idx ON products ((attributes->>'brand'));
SELECT * FROM products WHERE attributes->>'brand' = 'Nike';

-- jsonb_path_ops: 2-3x smaller, only supports @>
CREATE INDEX idx ON products USING gin (attributes jsonb_path_ops);
2. tsvector による全文検索
-- Add generated tsvector column
ALTER TABLE articles ADD COLUMN search_vector tsvector
  GENERATED ALWAYS AS (
    to_tsvector('english', coalesce(title,'') || ' ' || coalesce(content,''))
  ) STORED;

CREATE INDEX articles_search_idx ON articles USING gin (search_vector);

-- Fast full-text search
SELECT * FROM articles
WHERE search_vector @@ to_tsquery('english', 'postgresql & performance');

-- With ranking
SELECT *, ts_rank(search_vector, query) as rank
FROM articles, to_tsquery('english', 'postgresql') query
WHERE search_vector @@ query
ORDER BY rank DESC;

指摘すべきアンチパターン

❌ クエリのアンチパターン
  • 本番コードでの SELECT *
  • WHERE/JOIN 列にインデックスがない
  • 大きなテーブルに対する OFFSET ページネーション
  • N+1 クエリパターン
  • パラメーター化されていないクエリ(SQL インジェクションのリスク)
❌ スキーマのアンチパターン
  • ID に int を使用する(bigint を使用してください)
  • 理由なく varchar(255) を使用する(text を使用してください)
  • タイムゾーンなしの timestamp を使用する(timestamptz を使用してください)
  • 主キーにランダムな UUID を使用する(UUIDv7 または IDENTITY を使用してください)
  • 引用符が必要な大文字・小文字混在の識別子
❌ セキュリティのアンチパターン
  • アプリケーションユーザーへの GRANT ALL
  • マルチテナントテーブルで RLS が設定されていない
  • 行ごとに関数を呼び出す RLS ポリシー(SELECT でラップされていない)
  • RLS ポリシー列にインデックスがない
❌ 接続のアンチパターン
  • コネクションプーリングがない
  • アイドルタイムアウトがない
  • トランザクションモードのプーリングでプリペアドステートメントを使用する
  • 外部 API 呼び出し中にロックを保持する

レビューチェックリスト

データベースの変更を承認する前に:
  • すべての WHERE/JOIN 列にインデックスがある
  • 複合インデックスの列順が正しい
  • 適切なデータ型(bigint、text、timestamptz、numeric)を使用している
  • マルチテナントテーブルで RLS が有効になっている
  • RLS ポリシーで (SELECT auth.uid()) パターンを使用している
  • 外部キーにインデックスがある
  • N+1 クエリパターンがない
  • 複雑なクエリで EXPLAIN ANALYZE を実行している
  • 小文字の識別子を使用している
  • トランザクションを短く保っている

覚えておいてください:データベースの問題は、多くの場合、アプリケーションのパフォーマンス問題の根本原因です。クエリとスキーマ設計を早い段階で最適化してください。EXPLAIN ANALYZE を使用して想定を検証してください。外部キーと RLS ポリシー列には必ずインデックスを作成してください。

MIT ライセンスの下で Supabase Agent Skills を参考にしたパターンです。

コメント