やあ。今日もデータベースと格闘してる?
SQLを書いていて、「なぜこのクエリだけやたら遅いんだろう?」と悩むこと、あるよね。実行計画を見てみたら、おなじみの `Hash Join` ではなく `Merge Join` が選ばれている……そんな場面に出くわしたとき、君はどう感じているかな?
「あ、マージ結合か。これって何がいいんだっけ?」と一瞬でも頭をよぎるなら、この記事は君のためにある。今日は、PostgreSQLの職人として、マージ結合の「本質」と「実務での付き合い方」を伝授するよ。
—
1. マージ結合(Merge Join)って結局なんなのか?
教科書的な定義を言えば、「ソート済みの二つの入力を順次読み込みながら結合する方式」だよね。でも、現場ではこうイメージしてほしい。
「二つの長い巻物(ソート済みリスト)を、両手で同時に広げながら、同じ名前を見つけたらすかさずメモする」
これがマージ結合の動きだ。
Nested Loop(入れ子ループ)が「片方のリストを片手に、もう片方を最初から最後まで何度もなぞる」という地獄の作業だとしたら、マージ結合は、お互いがきれいに並んでいるおかげで、一度通り過ぎたら二度と戻らなくていい。だから、データ量が膨大になっても、計算コストが爆発しにくいんだ。
2. なぜPostgreSQLはマージ結合を選ぶのか
PostgreSQLのオプティマイザが「Hash JoinじゃなくてMerge Joinで行こう」と決断するとき、そこには明確な理由があるんだ。
- ソート済みであること(または、ソートコストが安いこと)
- すでにインデックスが効いているカラム同士の結合なら、PostgreSQLは追加のソートコストを払わずにマージを開始できる。これが最強のパターンだね。
- 等価結合(`=`)であること
- 不等号(`<` や `>`)を含んだ結合条件でも使えるのが、ハッシュ結合に対する大きなアドバンテージだ。これ、意外と忘れられがちだけど重要だよ。
- メモリが限られているとき
- ハッシュ結合は大きなハッシュテーブルをメモリ上に作る必要がある。メモリがカツカツの環境だと、ディスクに溢れて(Spill to disk)パフォーマンスがガタ落ちする。マージ結合はストリーミング処理に近いから、メモリ消費が安定しているんだ。
3. 実務で遭遇する「マージ結合の罠」
現場でよくある失敗ケースを一つ紹介しよう。
SELECT
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.created_at > ‘2023-01-01’;
もし `orders` テーブルの `customer_id` にインデックスがなかったらどうなる?
PostgreSQLは「よし、マージ結合のためにわざわざソートしよう」と考える。この「ソート」のコストが巨大だと、ハッシュ結合を選んだほうが早かった……なんてことはザラにある。
先輩からのアドバイス:
`EXPLAIN ANALYZE` を見たとき、`Sort` ノードが実行時間の大部分を占めていないか確認してほしい。もしそうなら、そのカラムにインデックスを貼るか、あるいは結合条件を見直す必要がある。
4. マージ結合を味方につけるためのコード戦略
マージ結合を「狙って」発動させるには、インデックス戦略が鍵になる。
例えば、マスタテーブルとトランザクションテーブルを結合する際、マスタ側でインデックスを貼っておくのは基本だよね。
— customer_id にインデックスがあれば、マージ結合は極めて高速に動く
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
こうしておけば、オプティマイザは「あ、これソート済みとして扱えるな」と判断して、スマートにマージ結合を選択してくれる。実行計画で `Merge Join` を見つけたら、「よし、インデックスがちゃんと活きてるな」とガッツポーズしていい。
最後に:データベースと会話しよう
技術スタックが進歩しても、マージ結合のような「基礎的なアルゴリズム」の重要性は変わらない。結局、データベースエンジンが何を考えているのかを想像できるかどうかが、エンジニアとしての腕の見せ所なんだ。
次に `EXPLAIN` を叩くときは、ただ数字を見るだけじゃなく、「今、PostgreSQLは二つの巻物を同時に広げているんだな」と想像してみてほしい。そうすれば、自ずとどんなインデックスが必要か、どんなクエリを書くべきかが見えてくるはずだよ。
何か詰まったら、またいつでも聞いてくれ。一緒に最高にクールなクエリを追求しようぜ。
コメント