変更要約: 初版(主題G2・G2.1〜G2.4)
2.3クエリ実行計画
実行計画を読み解く力を学びます。計画を可視化するEXPLAIN/実際に実行して実測値も出すEXPLAIN ANALYZE、結合戦略のNested Loop・Hash Join・Merge Join、スキャン種別のSeq Scan・Index Scan・Bitmap Scan、集計のウィンドウ関数、複数プロセスで処理を分担するパラレルクエリ(max_worker_processes・max_parallel_workers_per_gather)を押さえます。
これまでの節で見た統計情報は、プランナが実行計画を組み立てるための材料でした。この節はその出力である実行計画そのものを読む番です。EXPLAINはGold試験の中核であり、「どの結合戦略が」「どのスキャン種別で」「どの順序で」実行されるかを正確に読み取れることが、性能問題の切り分けに直結します。
2.3.1EXPLAIN と EXPLAIN ANALYZE の読み方
- EXPLAIN=クエリを実際には実行せず、プランナが選んだ実行計画をツリー形式で表示するコマンド。各ノードに
cost=開始コスト..総コスト(プランナの見積もり・単位は抽象的なコスト値)とrows=見積もり行数、width=平均行幅が付く。見積もり値のみで実測は含まれない。 - EXPLAIN ANALYZE=クエリを実際に実行した上で、各ノードの
actual time=開始時間..終了時間(ミリ秒)とactual rows=実測行数、loops=そのノードが実行された回数を追加表示する。見積もりと実測を突き合わせられるのが最大の価値で、rows(見積もり)とactual rows(実測)の乖離が大きいほど統計が古い/不正確な可能性が高い。副作用のあるDML(INSERT/UPDATE/DELETE)に使うと実際にデータが変更されるため、確認だけしたい場合はトランザクションで囲みROLLBACKするか、読み取り専用クエリに限定する。 - 計画ツリーは最も内側(インデントが深い)のノードから実行され、外側へ結果が渡される。
Seq Scanのようなリーフノードが実データを取得し、Hash Joinのような中間ノードがそれらを結合し、最上位のLimitやSortが最終整形を行う、という下から上への読み方が基本。
2.3.2結合戦略・スキャン種別・ウィンドウ関数・パラレルクエリ
- 結合戦略:Nested Loop(外側の各行に対し内側を毎回スキャン。小さいテーブル同士や内側にインデックスが効く場合に有利)/Hash Join(片方(通常小さい方)をメモリ上にハッシュテーブル化し、もう片方を1回スキャンして照合。等値結合かつ大量データで有利)/Merge Join(両方を結合キーでソート済みにしてから先頭から突き合わせる。両方がソート済み、またはソートコストを払っても見合う場合に選ばれる)。
- スキャン種別:Seq Scan(テーブル全体を先頭から順に読む。対象行が多い/インデックスが効かない場合の既定)/Index Scan(インデックスで該当行を探し、そのつどヒープへアクセスして行データを取得。対象行が少ない場合に有利)/Bitmap Scan(
Bitmap Index Scanでインデックスから該当ブロックのビットマップを作り、Bitmap Heap Scanでそのブロックだけをまとめて読む。対象行数が中間的でIndex Scanほど絞れずSeq Scanほど多くない場合に選ばれ、ヒープアクセスをブロック単位にまとめてランダムI/Oを減らす)。 - ウィンドウ関数=
OVER (PARTITION BY ... ORDER BY ...)により、集計しても行を潰さず各行に計算結果を付与する機能(ROW_NUMBER()・RANK()・累積和等)。実行計画では専用のWindowAggノードとして現れ、事前にPARTITION BY/ORDER BY列でのソートが必要になることが多い。 - パラレルクエリ=大規模なSeq Scanや集計を複数バックグラウンドワーカーに分担させる仕組み。max_worker_processes(サーバー全体で使えるバックグラウンドワーカーの総数上限)とmax_parallel_workers_per_gather(1つの
Gatherノードが使える並列ワーカー数上限)で制御し、実行計画にはGather(各ワーカーの結果を集約するノード)やParallel Seq Scanとして現れる。
「EXPLAINは見積もりのみ・EXPLAIN ANALYZEは実際に実行し実測値も出す(DMLは副作用に注意)」「Nested Loop=小規模/内側index有利・Hash Join=等値結合+大量データ・Merge Join=ソート済み前提」「Bitmap Scanは中間的な行数でSeq/Indexの折衷」「rowsとactual rowsの乖離は統計の陳腐化を疑う」「Gatherノード=パラレルクエリの集約点」が最頻出です。結合戦略とスキャン種別がそれぞれ独立した軸(結合方式の選択とデータ取得方式の選択)であることの理解が問われます。
実際のEXPLAIN ANALYZE出力を読む練習をしましょう。ordersテーブル(数百万行)とcustomersテーブル(数万行)をcustomer_idで結合しWHERE customers.region = 'tokyo'で絞る典型的なクエリを想定します。出力の最上位がHash Join (cost=1250.00..48000.00 rows=15000 width=64) (actual time=12.500..350.200 rows=14800 loops=1)であれば、この結合はHash Joinが選ばれ、見積もり15000行に対し実測14800行とほぼ一致しており、統計は健全だと判断できます。その下の子ノードがHash (cost=800.00..800.00 rows=5000 width=32) (actual time=8.000..8.000 rows=4820 loops=1)→Seq Scan on customers (cost=0.00..800.00 rows=5000 width=32) (actual time=0.010..6.500 rows=4820 loops=1) Filter: (region = 'tokyo'::text)であれば、小さい方のcustomersを絞り込んでからハッシュテーブル化し(Hashノード)、もう一方の子であるSeq Scan on orders (cost=0.00..42000.00 rows=3000000 width=40) (actual time=0.020..180.000 rows=3000000 loops=1)でordersを全件スキャンして照合していることが読み取れます。ここでもしrows=15000(見積もり)に対しactual rows=140000(実測)のように1桁以上の乖離があれば、regionカラムの統計が古いか、customers.regionとorders側の何らかのカラムに未登録の相関があると疑い、ANALYZE customers;の再実行や拡張統計の追加を検討します。また、同じクエリでもしインデックスidx_customers_regionがregionに存在し、対象行がテーブル全体のごく一部(例:全体の1%未満)であれば、プランナはSeq Scan on customersの代わりにIndex Scan using idx_customers_region on customers、あるいは対象行数が中間的であればBitmap Heap Scan on customers+Bitmap Index Scan on idx_customers_regionを選ぶ可能性が高く、同じテーブル・同じ条件でも行数の見積もりによってスキャン戦略が変わるという点が実行計画を読む上での要点です。集計を伴う分析クエリ(例:地域別の累積売上を求める)ではWindowAggノードが現れ、事前段階のSort(PARTITION BY/ORDER BY列でのソート)が計画に含まれているかを確認します。大規模なordersテーブル全体を集計する場合、Gatherノードの下にParallel Seq Scan on ordersが現れていればパラレルクエリが有効化され複数ワーカーで分担されており、max_parallel_workers_per_gatherの設定値がそのGatherノードで実際に使われたワーカー数の上限になります。
| 種別/戦略 | 選ばれやすい条件 | 計画上の表記 |
|---|---|---|
| Nested Loop | 小規模データ・内側に有効なインデックス | Nested Loop |
| Hash Join | 等値結合・大量データ | Hash Join + Hash |
| Merge Join | 両側ソート済み/ソート許容 | Merge Join + Sort |
| Seq Scan | 対象行が多い/index不使用 | Seq Scan on <table> |
| Index Scan | 対象行が少ない | Index Scan using <idx> |
| Bitmap Scan | 対象行数が中間的 | Bitmap Heap Scan + Bitmap Index Scan |
ひっかけ: 「EXPLAINだけでクエリの実際の所要時間が分かる」は誤りです。EXPLAINは見積もりのみで実測時間は含まれず、実測が必要ならEXPLAIN ANALYZEを使う必要があります。また「EXPLAIN ANALYZEはクエリを実行しないので安全にDMLへ使える」も誤り=EXPLAIN ANALYZEは実際にクエリを実行するため、DML(INSERT/UPDATE/DELETE)に使うとデータが実際に変更されます。確認目的ならトランザクションで囲みROLLBACKするなどの配慮が要ります。
2.3.3この節のまとめ
- EXPLAIN=見積もりのみ・EXPLAIN ANALYZE=実行して実測値も出す(DML副作用に注意)。計画は下(内側)から上へ読む
- 結合戦略(Nested Loop/Hash Join/Merge Join)とスキャン種別(Seq/Index/Bitmap)は独立した軸。WindowAgg=ウィンドウ関数・Gather=パラレルクエリの集約(max_worker_processes/max_parallel_workers_per_gatherで制御)
進捗の記録にはログインが必要です。
理解度チェック
(軽い確認用)Q1. 実行計画の見積もりだけでなく、実際に実行した際の所要時間や実測行数も確認したい。使うべきコマンドは?
Q2. 数万行のテーブルと数百万行のテーブルを等値結合するクエリで、実行計画には小さい方のテーブルをメモリ上のハッシュテーブルに変換するノードが含まれていた。この結合戦略として最も妥当なものは?
Q3. 大規模な orders テーブル全体を対象にした集計クエリの実行計画に Gather ノードと Parallel Seq Scan on orders が現れた。この計画について正しい説明は?

