PostgreSQLバックエンドパフォーマンス
PostgreSQL クエリ最適化入門 — スロークエリを撲滅する5つのアプローチ
EXPLAIN ANALYZE の読み方から始まり、インデックス設計・N+1 問題の解消・パーティショニングまで、本番環境で実際に効果があったチューニング手法をまとめました。
スロークエリの調査から始める
最適化の第一歩は「何が遅いのか」を正確に把握することです。PostgreSQL には EXPLAIN ANALYZE というクエリ実行計画を表示するコマンドがあります。
EXPLAIN ANALYZE
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id;
出力の見方:
- Seq Scan: テーブル全件スキャン(インデックスが効いていないサイン)
- Index Scan: インデックスを使ったスキャン(効率的)
- actual time: 実際の実行時間
- rows: 処理した行数
1. インデックスを適切に張る
最も効果的な最適化はインデックス設計です。WHERE 句や JOIN の条件に使うカラムにはインデックスを張りましょう。
-- 外部キーには必ずインデックスを
CREATE INDEX idx_orders_user_id ON orders(user_id);
-- 複合インデックスはカーディナリティの高い順に
CREATE INDEX idx_orders_status_created ON orders(status, created_at);
2. N+1 問題を解消する
ORM を使うと無意識に N+1 クエリが発生しがちです。
-- 悪い例(N+1)
SELECT * FROM posts;
-- 各 post に対して:
SELECT * FROM users WHERE id = ?;
-- 良い例(JOIN で一括取得)
SELECT p.*, u.name AS author_name
FROM posts p
JOIN users u ON u.id = p.author_id;
3. 部分インデックスで絞り込む
全レコードではなく、特定の条件に一致するレコードのみのインデックスを作ることで、サイズを抑えつつ効果を高められます。
-- 未処理の注文のみインデックス化
CREATE INDEX idx_orders_pending
ON orders(created_at)
WHERE status = 'pending';
4. VACUUM / ANALYZE を定期実行する
PostgreSQL は削除・更新したレコードをすぐに物理削除しません。VACUUM で不要領域を回収し、ANALYZE で統計情報を更新することで、クエリプランナーが最適な実行計画を立てられます。
-- 統計情報の更新
ANALYZE users;
-- 不要領域の回収 + 統計更新
VACUUM ANALYZE orders;
autovacuum が有効な環境では自動実行されますが、大量更新後は手動で実行するのがおすすめです。
5. パーティショニングで大テーブルを分割する
数億件を超えるような大テーブルは、パーティショニングで分割することでクエリ性能を改善できます。
-- 月ごとにパーティション分割
CREATE TABLE events (
id BIGSERIAL,
occurred_at TIMESTAMPTZ NOT NULL,
payload JSONB
) PARTITION BY RANGE (occurred_at);
CREATE TABLE events_2024_01
PARTITION OF events
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
クエリで occurred_at の範囲を指定すれば、対象パーティションのみスキャンされるため劇的に高速化されます。
まとめ
クエリ最適化はまず EXPLAIN ANALYZE で現状を把握し、ボトルネックを特定してから対処するのが基本です。闇雲にインデックスを増やすと書き込みパフォーマンスが低下するので、読み取りと書き込みのトレードオフを意識して設計しましょう。