読者が抱える課題
「SQLの実行速度が遅い」「インデックスを追加したのにクエリが高速化しない」といった問題に直面した際、原因を特定できずに勘に頼った修正を行ってしまうことがあります。PostgreSQLでは、実行計画(Execution Plan)を正しく読み解くことで、ボトルネックがスキャン方法にあるのか、結合アルゴリズムにあるのかを客観的に特定できます。
この記事で分かること
-
EXPLAINコマンドの適切な実行方法とオプションの使い分け - 実行計画に現れる主要なスキャン・結合アルゴリズムの読み解き方
- ボトルネックを特定するための判断基準と具体的な改善手順
- 実務で使えるクエリ改善チェックリスト
対象読者・前提条件
- PostgreSQLを使用した開発・運用経験があるエンジニア
- 基本的なSQL(SELECT、JOIN、WHERE句など)を理解している方
- 検証環境は PostgreSQL 12 以降を想定しています(バージョンにより一部の出力フォーマットや機能が異なる場合があります。最新の仕様は公式ドキュメントを参照してください)。
1. 実行計画を取得する基本コマンド
PostgreSQLで実行計画を取得するには EXPLAIN コマンドを使用します。実務でボトルネックを特定する際は、実際にクエリを実行して詳細な統計情報を取得する ANALYZE オプションを併用することが推奨されます。
推奨される実行コマンド例
EXPLAIN (ANALYZE, BUFFERS, COSTS, TIMING, SUMMARY)
SELECT user_id, order_date, total_amount
FROM orders
WHERE user_id = 12345 AND order_date >= '2023-01-01';
各オプションの役割
- ANALYZE: クエリを実際に実行し、実際の実行時間(actual time)や処理された行数(rows)を測定します。※データの更新・削除を伴うクエリ(INSERT/UPDATE/DELETE)に実行すると、実際にデータが書き換わるため、トランザクション制御(ROLLBACK)を併用するなどの注意が必要です。
- BUFFERS: 共有バッファのヒット数やディスクからの読み込みブロック数を表示します。I/Oボトルネックの特定に不可欠です。
- COSTS: 各ノードの推定コスト(起動コスト、総コスト)を表示します。
- TIMING: 各ノードの開始時間と終了時間をミリ秒単位で測定します。
- SUMMARY: 実行計画の作成時間(Planning Time)と実行時間(Execution Time)のサマリーを出力します。
2. 実行計画の主要な要素と読み解き方
実行計画はツリー構造で出力されます。インデントが深い(内側の)ノードから順に実行されます。
代表的なスキャン方法(Scan Methods)
| スキャン名 | 特徴 | 改善の方向性 |
|---|---|---|
| Seq Scan | テーブル全体を先頭から順に走査する。 | 取得件数が少ない場合は、適切なインデックスの作成を検討する。 |
| Index Scan | インデックスを走査して該当行のポインタを得た後、テーブルのデータブロックにアクセスする。 | 取得件数が少ない場合に効率的。ランダムアクセスが発生する。 |
| Index Only Scan | インデックス内の情報のみでクエリの結果を返せるため、テーブル本体へのアクセスを省略する。 | カバリングインデックス(INCLUDE句など)の活用を検討する。 |
| Bitmap Index Scan / Bitmap Heap Scan | 複数のインデックス条件をメモリ上でビットマップとして結合し、テーブルへのアクセスをまとめて行う。 | 大量のデータをインデックス経由で取得する際によく発生する。 |
代表的な結合アルゴリズム(Join Operators)
- Nested Loop: 外側のテーブルの1行ごとに、内側のテーブルを走査する。内側のテーブルに適切なインデックスがあり、外側の行数が少ない場合に高速です。
-
Hash Join: 片方のテーブルからメモリ上にハッシュテーブルを作成し、もう片方のテーブルと結合する。大量データの結合に適していますが、メモリ(
work_mem)を消費します。 - Merge Join: 結合キーでソートされた2つのテーブルを順にスキャンして結合する。事前にソートされている場合や、インデックスによるソート順が利用できる場合に有効です。
3. クエリ改善の具体的な手順
ボトルネックを特定し、クエリを改善する手順は以下の通りです。
ステップ1:Planning Time と Execution Time の比較
- Planning Time が異常に長い場合: パーティション数が多すぎる、または結合テーブル数が多すぎてオプティマイザの探索空間が広がりすぎている可能性があります。
- Execution Time が長い場合: 実際の処理に時間がかかっています。ステップ2へ進みます。
ステップ2:推定行数(rows)と実際の行数(actual rows)の乖離を確認
-
EXPLAINの出力にあるrows=...(推定値)とactual rows=...(実測値)を比較します。 -
乖離が大きい場合(例: 10倍以上の差): 統計情報が古い、または複雑な条件式によりオプティマイザが正しく見積もれていません。
ANALYZEコマンドを実行して統計情報を更新するか、結合順序の固定などを検討します。
ステップ3:I/Oバッファ使用量の確認
-
BUFFERSの出力でshared read(ディスクからの読み込み)が多い箇所を特定します。shared hit(キャッシュヒット)を増やすために、インデックスの追加や、不要なカラムをSELECT対象から除外することを検討します。
4. 実務用クエリ改善チェックリスト
クエリのパフォーマンスに問題が発生した際、以下の項目を順に確認してください。
-
統計情報は最新か
- 対象テーブルに対して最近
ANALYZEが実行されているか確認する。
- 対象テーブルに対して最近
-
不要な Seq Scan が発生していないか
- 検索条件(WHERE句)や結合条件(ON句)の列にインデックスが定義されているか。
-
インデックスが効かない書き方をしていないか
-
WHERE CHAR_LENGTH(user_name) > 10のように、インデックス列に関数を適用していないか(関数インデックスの検討が必要)。 - 暗黙の型変換が発生していないか(例: 文字列型の列に数値を渡しているなど)。
-
-
ソート処理がメモリ内(work_mem)で収まっているか
-
Sort Method: external merge Diskのような出力がある場合、メモリ不足で一時ファイルがディスクに書き出されています。work_memの一時的な拡張や、インデックスを利用したソートへの代替を検討します。
-
-
不要な結合やカラム取得がないか
-
SELECT *を避け、必要なカラムのみを指定しているか。
-
5. 注意点と運用上の判断基準
-
インデックスの追加は慎重に行う
インデックスを追加すると、参照(SELECT)は高速化しますが、更新(INSERT/UPDATE/DELETE)のオーバーヘッドが増加します。書き込み頻度の高いテーブルへのインデックス追加は、システム全体の負荷バランスを考慮して決定してください。 -
開発環境と本番環境のデータ量の差
データ量が極端に少ない開発環境では、オプティマイザがインデックスを使用せず、あえて高速なSeq Scanを選択することがあります。実行計画の検証は、可能な限り本番環境に近いデータ量・データ分布を再現した環境で行ってください。
まとめ
PostgreSQLのクエリ改善は、あてずっぽうにインデックスを貼るのではなく、EXPLAIN (ANALYZE, BUFFERS) を用いてボトルネックを正確に特定することから始まります。推定値と実測値の乖離や、I/Oバッファの状況を観察し、適切なインデックス設計やクエリの書き換えを行ってください。