一般社団法人 全国個人事業主支援協会

COLUMN コラム

  • PostgreSQLのパフォーマンスチューニング:インデックス戦略と実行計画の読み方

PostgreSQLパフォーマンスチューニングの基本方針

PostgreSQLは高機能なRDBMSですが、適切なチューニングなしでは本来の性能を発揮できません。特にデータ量が増加してくると、インデックスの有無や設計がクエリ性能に劇的な差をもたらします。本記事では、実務でよく遭遇するパフォーマンス問題に対して、インデックス戦略と実行計画(EXPLAIN)の読み方を中心に解説します。

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

実行計画の読み方のポイント

実行計画を読む際に注目すべき重要な項目があります。

  • actual time:実際にかかった時間(ミリ秒)。最初の数値が最初の行の取得時間、2番目が全行の取得完了時間です。
  • rows:推定行数と実際の行数の乖離。大きく異なる場合は統計情報が古い可能性があります。
  • Seq Scan:テーブルの全行走査。大きなテーブルでこれが出ている場合は要注意です。
  • Rows Removed by Filter:フィルタで除外された行数。この値が大きい場合、インデックスの追加で改善できる可能性があります。

上の例では、ordersテーブルに対してSeq Scanが実行されており、約97万行がフィルタで除外されています。これは明らかにインデックスが必要なシグナルです。

インデックス戦略の実践

基本的なB-treeインデックス

最も汎用的なインデックスタイプです。等値検索、範囲検索、ソートに効果を発揮します。

-- 単一カラムインデックス
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ですが、カーディナリティが高いカラムや分布に偏りがあるカラムでは、この値を増やすことで精度が向上します。

実践的なチューニング手順

実務でのパフォーマンスチューニングは、以下の手順で進めることをお勧めします。

  1. スロークエリの特定pg_stat_statements拡張を有効にし、実行時間や呼び出し回数が多いクエリを特定する
  2. 実行計画の分析EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)で詳細な実行計画を取得する
  3. ボトルネックの特定:Seq Scan、大量のRows Removed、高いバッファ読み取り数などに注目する
  4. インデックスの設計:ボトルネックに対して適切なインデックスを作成する
  5. 効果の検証:再度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のインデックスを定期的に確認し、不要なものは削除する
  • インデックスの肥大化:大量のUPDATE/DELETEが行われるテーブルではインデックスが肥大化します。REINDEXコマンドで再構築できますが、運用中はロックに注意が必要です
  • CONCURRENTLY オプション:本番環境では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や大量のフィルタ除外行などのシグナルを見逃さないことが重要です。複合インデックス、部分インデックス、カバリングインデックスを適切に使い分けることで、多くのパフォーマンス問題を解決できます。ただし、インデックスの追加は書き込み性能とのトレードオフであることを常に意識し、本当に必要なインデックスだけを維持する姿勢が大切です。

この記事をシェアする

  • Twitterでシェア
  • Facebookでシェア
  • LINEでシェア