やあ。今日もデータベースの泥沼、いや、深淵と格闘しているかな?
PostgreSQLのクエリチューニングの話になると、みんな「まずはインデックスを貼ろう」って言うよね。もちろんそれは正しい。でも、いざ数百万件、数千万件というオーダーのデータをJOINしようとしたとき、インデックスだけではどうにもならない壁にぶつかることがある。
今日は、そんなときに避けては通れない「ハッシュ結合(Hash Join)」の話をしよう。これを知っているかどうかで、深夜の「クエリが終わらない」という悪夢から解放される確率がグッと上がるはずだ。
—
ハッシュ結合という「戦略」
そもそもハッシュ結合って何をしているのか。簡単に言えば、「小さい方のテーブルをメモリ上に展開して辞書(ハッシュテーブル)を作り、もう片方のテーブルをスキャンしながら突き合わせる」という戦略だ。
ネステッドループ(Nested Loop)が「Aの全行に対してBを全走査する」という力技なのに対し、ハッシュ結合は「メモリに地図を広げておいて、相手が通り過ぎる瞬間に答えを出す」という、非常に効率的なやり方なんだ。
`work_mem` は「作業場の広さ」だ
ここで登場するのが、PostgreSQLの設定値の中でも特に重要な `work_mem` だ。
ハッシュ結合において、`work_mem` は「ハッシュテーブルを置くための作業机の広さ」だと思っていい。もしこの机が十分に広ければ、ハッシュテーブルはすべてメモリ上に収まる。これを In-Memory Hash Join と呼ぶ。これが理想だ。
しかし、もし `work_mem` が足りなくて、ハッシュテーブルが入りきらなくなったらどうなるか? データベースはあふれた分をディスク(一時ファイル)に書き出し始める。これを On-Disk Hash Join と呼ぶ。
想像してみてほしい。メモリ上で超高速に処理できるはずのものが、ディスクへの書き込み・読み込みが発生した瞬間に、数倍、数十倍の遅延を引き起こす。これが「クエリが突然重くなる」現象の正体の一つだ。
チューニングの現場:どう向き合うか
実務でクエリを最適化する際、まずは `EXPLAIN (ANALYZE, BUFFERS)` を叩くのが鉄則だ。
EXPLAIN (ANALYZE, BUFFERS)
SELECT FROM orders o
JOIN line_items li ON o.id = li.order_id;
もし、出力結果にこんな表示があったら要注意だ。
Hash Join (cost=… rows=… width=…)
Batches: 2 Memory Usage: 1024kB Disk: 512kB
ここで注目すべきは `Batches` と `Disk` の項目だ。`Batches` が1より大きかったり、`Disk` に値が入っている場合、そのクエリはメモリ不足で苦しんでいる。
具体的な対策のステップ
1. 特定クエリの `work_mem` を一時的に広げる
サーバー全体の設定値をいきなり変えるのはリスクが高い。まずは特定のトランザクションだけ、あるいはセッションだけ広げて様子を見るのがプロのやり方だ。
BEGIN;
SET LOCAL work_mem = ’64MB’;
— ここで重いクエリを実行
EXPLAIN ANALYZE SELECT … ;
COMMIT;
2. テーブルの統計情報を最新にする
PostgreSQLは「どっちが小さいテーブルか」を統計情報で判断している。`ANALYZE` を忘れていて、プランナが大きなテーブルをハッシュテーブルにしようとしてメモリを溢れさせているケースは驚くほど多い。
3. `work_mem` を上げすぎない
「じゃあ `work_mem` を1GBとかにしちゃえばいいじゃん!」と思うかもしれない。でも注意してくれ。`work_mem` は「同時実行数 × クエリ内の結合ノード数」で消費される。もし `work_mem` を大きくしすぎて同時接続数が増えたら、サーバーのメモリが一瞬で枯渇して、OSがOOM Killerを発動させ、データベースごと落ちる。それは一番やってはいけないミスだ。
最後に:バランス感覚こそがエンジニアの武器
ハッシュ結合のチューニングは、メモリとディスクのトレードオフとの戦いだ。
「とりあえず値を大きくする」のではなく、「このクエリがメモリ上で完結するために最低限必要なサイズはどれくらいか?」をプランナの推測値(Estimated Rows)と突き合わせながら計算していく。この地味な作業こそが、実は一番の近道なんだ。
データベースは嘘をつかない。`EXPLAIN` の結果という「現場の証拠」をしっかり読み解けば、必ず最適解は見つかる。
もし今、君のクエリがディスクを叩いて悲鳴を上げているなら、まずは `work_mem` の様子を伺いに行ってみてくれ。健闘を祈るよ!
コメント