マージ結合の「魔法」を解く:なぜPostgreSQLはインデックスを愛するのか
現場でクエリの遅延を調査していると、必ずと言っていいほど「マージ結合(Merge Join)」という壁にぶつかります。
ハッシュ結合(Hash Join)が、メモリを豪快に食いつぶしながら力技で突き進む重戦車だとすれば、マージ結合は、整然と並んだデータの上を優雅に滑る熟練のダンサーです。しかし、このダンサーは「ソート」という足枷を嫌う。この特性を理解しているかどうかで、データベースのパフォーマンスは天と地ほどの差が出ます。
今回は、PostgreSQLにおけるマージ結合の深淵、そして「ソート回避」がもたらす最適化の真髄についてお話ししましょう。
マージ結合のアーキテクチャ:その「反復」の正体
マージ結合のロジックは極めてシンプルです。双方が結合キーでソート済みであるという前提のもと、片方の行を読みながら、もう片方の行をポインタでなぞっていく。この「線形的な走査」こそが、マージ結合の最大の武器です。
しかし、もしデータがソートされていなかったら?
PostgreSQLのオプティマイザは、迷わず`Sort`ノードを挿入します。ここがトラブルの温床です。
- Sortのコスト: `work_mem`を超えたソートは、たちまちディスク(一時ファイル)への退避を引き起こします。I/Oのレイテンシはメモリの数桁上。クエリが「突然」遅くなる原因のほとんどはここです。
- Pipelineの断絶: ソートが完了するまで、結合処理は一歩も前へ進めません。結果として、最初の1行が返ってくるまでの時間(Time to First Row)が極端に長くなります。
「ソート済み」を神格化せよ
私たちが目指すべき理想は、実行計画から`Sort`ノードを完全に排除することです。そして、そのための最も強力な武器が「インデックス」です。
B-treeインデックスは、それ自体がすでにソート済みの構造を持っています。オプティマイザにこれを使わせない手はありません。
インデックスによるソート回避のメカニズム
PostgreSQLのプランナーが賢いのは、単にテーブルのスキャンにインデックスを使うだけでなく、結合の「入力元」としてインデックスを利用することで、ソート処理を完全にスキップする計画を立てられる点です。
例えば、以下のようなクエリを考えてみてください。
SELECT FROM orders o
JOIN line_items l ON o.id = l.order_id
ORDER BY o.id;
もし `orders.id` と `line_items.order_id` に適切なB-treeインデックスがあれば、PostgreSQLはマージ結合を選択し、かつ`Sort`ノードを一切出しません。インデックスの順序をそのまま利用してマージする、いわば「ゼロコスト・ソート」です。
パフォーマンストラブルシューティング:現場の視点
では、マージ結合が遅い時、現場ではどうアプローチすべきか。私の手順は決まっています。
1. `EXPLAIN (ANALYZE, BUFFERS)` を叩く:
まず見るべきは、`Sort`ノードの有無です。もしそこにあるなら、メモリ不足によるディスク溢れが発生していないか確認します。`Sort Method: external merge` という文字が見えたら、それはもう赤信号です。
2. インデックスの「形」を疑う:
単にインデックスがあるだけでは不十分です。結合キーの順序と、その後のフィルタやソートの要件が一致しているか? 複合インデックスの列順序は適切か?
3. 統計情報の鮮度:
オプティマイザがマージ結合を選択すべき場面で、誤ってハッシュ結合を選んでいたり、あるいはその逆だったりする場合、統計情報が古いケースが多々あります。`ANALYZE`を手動で実行するだけで劇的に改善することは、往々にしてあります。
最後に:職人の勘所
マージ結合を使いこなすことは、SQLチューニングにおける「洗練」です。
無闇にインデックスを貼るのではなく、データアクセスのパターンを読み解き、結合の入力側が「すでに整列している」という状態をいかに自然に作り出すか。そこに、データベースエンジニアとしての美学があるように思います。
「なぜこのクエリはここでソートしているのか?」
その疑問を突き詰めた先に、システムのボトルネックを解消する鍵が隠されているはずです。
さて、あなたのクエリ実行計画には、不要なソートは潜んでいませんか? 次のデプロイの前に、ぜひもう一度眺めてみてください。
コメント