PostgreSQLは高機能なRDBMSですが、適切なチューニングなしでは本来の性能を発揮できません。特にデータ量が増加してくると、インデックスの有無や設計がクエリ性能に劇的な差をもたらします。本記事では、実務でよく遭遇するパフォーマンス問題に対して、インデックス戦略と実行計画(EXPLAIN)の読み方を中心に解説します。
パフォーマンスチューニングの第一歩は、クエリが実際にどのように実行されているかを把握することです。PostgreSQLではEXPLAIN ANALYZEコマンドで実行計画と実際の実行時間を確認できます。
EXPLAIN ANALYZE
SELECT u.name, o.total_amount
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.created_at >= '2024-01-01'
AND o.status = 'completed'
ORDER BY o.total_amount DESC
LIMIT 100;
実行計画の出力例を見てみましょう。
Limit (cost=15234.56..15234.81 rows=100 width=48) (actual time=245.123..245.156 rows=100 loops=1)
-> Sort (cost=15234.56..15298.12 rows=25423 width=48) (actual time=245.121..245.138 rows=100 loops=1)
Sort Key: o.total_amount DESC
Sort Method: top-N heapsort Memory: 35kB
-> Hash Join (cost=1523.45..14567.89 rows=25423 width=48) (actual time=12.345..198.765 rows=25234 loops=1)
Hash Cond: (o.user_id = u.id)
-> Seq Scan on orders o (cost=0.00..12345.67 rows=25423 width=28) (actual time=0.012..178.234 rows=25234 loops=1)
Filter: ((created_at >= '2024-01-01') AND (status = 'completed'))
Rows Removed by Filter: 974766
-> Hash (cost=1234.00..1234.00 rows=50000 width=24) (actual time=12.123..12.123 rows=50000 loops=1)
-> Seq Scan on users u (cost=0.00..1234.00 rows=50000 width=24) (actual time=0.008..6.543 rows=50000 loops=1)
Planning Time: 0.456 ms
Execution Time: 245.234 ms
実行計画を読む際に注目すべき重要な項目があります。
上の例では、ordersテーブルに対してSeq Scanが実行されており、約97万行がフィルタで除外されています。これは明らかにインデックスが必要なシグナルです。
最も汎用的なインデックスタイプです。等値検索、範囲検索、ソートに効果を発揮します。
-- 単一カラムインデックス
CREATE INDEX idx_orders_created_at ON orders(created_at);
-- 複合インデックス(カラム順序が重要)
CREATE INDEX idx_orders_status_created ON orders(status, created_at);
複合インデックスでは、カラムの順序が極めて重要です。原則として、等値条件で使用するカラムを先に、範囲条件で使用するカラムを後に配置します。上の例では、status = 'completed'が等値条件、created_at >= '2024-01-01'が範囲条件なので、statusを先にしています。
特定の条件を満たす行だけにインデックスを作成する部分インデックスは、インデックスサイズの削減とクエリ性能の両方に効果的です。
-- statusが'completed'の注文のみインデックス
CREATE INDEX idx_orders_completed ON orders(created_at)
WHERE status = 'completed';
statusの値が限られていて、特定の値に対するクエリが多い場合、部分インデックスはフルインデックスよりも小さく高速です。
クエリに必要なすべてのカラムをインデックスに含めることで、テーブル本体へのアクセス(ヒープフェッチ)を回避できます。
-- INCLUDEでカバリングインデックスを作成
CREATE INDEX idx_orders_covering ON orders(status, created_at)
INCLUDE (total_amount, user_id);
このインデックスにより、先ほどのクエリはIndex Only Scanで実行される可能性が高くなり、大幅なパフォーマンス改善が期待できます。
PostgreSQLのクエリプランナは統計情報をもとに最適な実行計画を選択します。統計情報が古いと、不適切なプランが選ばれてしまいます。
-- テーブルの統計情報を手動更新
ANALYZE orders;
-- 特定カラムの統計情報の精度を上げる
ALTER TABLE orders ALTER COLUMN status SET STATISTICS 1000;
デフォルトの統計サンプル数は100ですが、カーディナリティが高いカラムや分布に偏りがあるカラムでは、この値を増やすことで精度が向上します。
実務でのパフォーマンスチューニングは、以下の手順で進めることをお勧めします。
pg_stat_statements拡張を有効にし、実行時間や呼び出し回数が多いクエリを特定するEXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)で詳細な実行計画を取得するEXPLAIN ANALYZEで改善を確認する-- pg_stat_statementsでスロークエリを確認
SELECT query,
calls,
mean_exec_time,
total_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;
インデックスは万能ではありません。過剰なインデックスはINSERT、UPDATE、DELETEの性能を低下させ、ディスク使用量も増加します。
pg_stat_user_indexesビューでidx_scanが0のインデックスを定期的に確認し、不要なものは削除するREINDEXコマンドで再構築できますが、運用中はロックに注意が必要ですCREATE INDEX CONCURRENTLYを使い、テーブルロックを回避する-- 未使用インデックスの確認
SELECT schemaname, tablename, indexname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0
AND indexname NOT LIKE '%_pkey'
ORDER BY pg_relation_size(indexrelid) DESC;
PostgreSQLのパフォーマンスチューニングは、実行計画の読解力とインデックス設計の知識が両輪となります。まずはEXPLAIN ANALYZEでクエリの実態を把握し、Seq Scanや大量のフィルタ除外行などのシグナルを見逃さないことが重要です。複合インデックス、部分インデックス、カバリングインデックスを適切に使い分けることで、多くのパフォーマンス問題を解決できます。ただし、インデックスの追加は書き込み性能とのトレードオフであることを常に意識し、本当に必要なインデックスだけを維持する姿勢が大切です。