マージ結合を制するものは、PostgreSQLの「重いクエリ」を制する
やあ。最近、実行計画(EXPLAIN)と睨めっこして頭を抱えてるエンジニアは多いんじゃないかな?
普段、何も意識せずにクエリを投げていると、PostgreSQLは賢いオプティマイザがよしなに結合方法を選んでくれる。でもね、データ量が数百万、数千万件と増えてくると、その「よしなに」が時として致命的なパフォーマンス低下を招くことがあるんだ。
今日は、そんな時に「切り札」になり得る「マージ結合(Merge Join)」について話をしよう。教科書的な定義じゃなくて、現場でどういう時に思い出して、どう使いこなすべきか。そこを深掘りしていくよ。
—
マージ結合って、結局何者なの?
一言で言うと、「両方のテーブルが『ソート済み』であるという強みを活かした、効率的な突き合わせ作業」だ。
イメージしてみてほしい。バラバラに散らばったトランプの山からペアを探すのは大変だよね? でも、両方の山が既に数字順に並んでいれば、上から一枚ずつめくって、「お、君は5か。こっちも5だね、じゃあ結合!」と、非常にスムーズに処理できる。
これがマージ結合の正体だ。
マージ結合のメリットと弱点
- メリット: ネステッドループのように「片方を全スキャンして、もう片方を毎回インデックス検索する」といったコストを繰り返さない。線形時間(O(N+M))で済むから、大規模な結合には非常に強い。
- 弱点: 「ソート」が前提だ。もし結合対象がインデックスでソートされていない場合、わざわざメモリ上で巨大なソート処理(Sortノード)が発生する。これが重いと、マージ結合の恩恵が帳消しになるどころか、メモリ不足でディスクに書き出し(External Sort)が始まって悲惨なことになる。
—
どんな時に「おっ、マージ結合が使えそうだな」と気づくべきか
実務でこんなケースに遭遇したら、マージ結合を疑ってみてくれ。
1. 大量データの等価結合: インデックスが効きにくい大規模な結合。
2. 不等号結合: 「A.val < B.val」みたいな、普通のハッシュ結合では対応できない結合条件のとき。マージ結合はソート済みなら不等号でもスキャンできるんだ。これが地味に最強の武器になる。
実践的なコード例
例えば、`logs`テーブルと`users`テーブルを結合して、特定の期間のログを抽出するとしよう。
— どちらもIDでソートされていると仮定
SELECT l.log_id, u.user_name
FROM logs l
JOIN users u ON l.user_id = u.user_id
WHERE l.created_at > ‘2023-01-01’;
もし、`logs(user_id)` と `users(user_id)` にインデックスが貼られていれば、PostgreSQLは迷わずマージ結合を選択してくれるはずだ。実行計画に `Merge Join` が出てきたら、「お、わかってるね」と心の中でガッツポーズしていい。
—
現場で直面する「落とし穴」
僕が後輩のコードレビューをしていてよく見るミスがこれだ。
「結合キーにインデックスを貼ればマージ結合になるだろう」と信じ込んでいるケース。
インデックスを貼るだけじゃダメなんだ。クエリの条件やデータの分布によっては、オプティマイザが「わざわざソートするコストを払うより、ハッシュ結合の方が速いな」と判断して、マージ結合を避けることがよくある。
もしマージ結合を強制したくなったら、まずは `EXPLAIN ANALYZE` を叩こう。そこで「Sort」にどれだけ時間がかかっているか確認するんだ。ソートに時間がかかっているなら、結合キーに適切なインデックスを貼って、PostgreSQLに「ソート済みであること」を教えてあげるのが最短ルートだよ。
まとめ:マージ結合は「準備」が全て
マージ結合は、魔法の杖じゃない。「事前の準備(インデックスによるソート)」というコストを先に払う代わりに、結合実行時の爆速を手に入れる手法なんだ。
- 大規模なテーブルをガッツリ結合したい。
- 不等号結合でクエリが死んでいる。
そんな時は、一度立ち止まって「どうすればデータがソートされた状態で結合できるか?」を考えてみてほしい。インデックスの設計思想が変わるはずだ。
データベースエンジニアとしての醍醐味は、こういう「仕組み」を理解して、オプティマイザと対話することにある。今日の話が、君のクエリ最適化のヒントになれば嬉しいよ。
また現場で会おう。健闘を祈る!
コメント