Blog 一覧に戻る
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;

出力の見方:

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 で現状を把握し、ボトルネックを特定してから対処するのが基本です。闇雲にインデックスを増やすと書き込みパフォーマンスが低下するので、読み取りと書き込みのトレードオフを意識して設計しましょう。