「PostgreSQLのハッシュ結合、なんとなく使ってない?」実務で差がつくチューニングの極意
こんにちは。現場でPostgreSQLと格闘している皆さん、お疲れ様です。
今日は、SQLを書くときには避けて通れない、でも意外と中身を意識していない人が多い「ハッシュ結合(Hash Join)」について、少しだけ深掘りしてみようと思います。
「EXPLAIN ANALYZEを見たらHash Joinって出てきたけど、とりあえず動いてるからいいや」と思っていませんか? それ、実はもっと速くできるかもしれませんよ。
—
ハッシュ結合って、結局何してるの?
教科書的な定義はさておき、現場レベルで分かりやすく言うと、「小さい方のテーブルでカンニングペーパー(ハッシュテーブル)を作って、大きい方のテーブルをそれと照らし合わせる」手法です。
1. Build Phase (構築フェーズ): 小さい方のテーブル(Inner側)をスキャンして、結合キーをハッシュ値に変換し、メモリ上にハッシュテーブルを作ります。
2. Probe Phase (プローブフェーズ): 大きい方のテーブル(Outer側)を一行ずつ読み込み、同じくハッシュ値を計算して、さっき作ったハッシュテーブルに合致するデータがないか確認します。
これの最大のメリットは、「両方のテーブルを何度もループしなくていい」こと。ネステッドループ結合(Nested Loop)だと地獄のような計算量になるケースでも、ハッシュ結合なら圧倒的な速さで結果を返してくれます。
—
実務で「ハッシュ結合」を意識すべき瞬間
僕がパフォーマンスチューニングを依頼されたとき、真っ先にチェックするのがこのハッシュ結合の効率です。特に意識してほしいポイントは2つ。
1. メモリ(work_mem)の枯渇に注意
ハッシュテーブルは、基本的に`work_mem`というメモリ領域に載ります。もしテーブルが巨大でメモリに収まらないとどうなるか? PostgreSQLはディスクに退避(スピル)させます。
「Hash Joinなのに異常に遅い」という場合、大抵はこれが原因です。
`EXPLAIN ANALYZE`の結果を見て、以下のようなメッセージが出ていたら要注意。
Batches: 5 Memory Usage: 10240kB Disk: 20480kB
「Batches: 1」ならメモリ内で完結していますが、数値が増えていたらディスクに書き出しています。この場合、そのセッションの`work_mem`を増やすか、クエリを分割するなどの対策が必要です。
2. 統計情報の鮮度
PostgreSQLのオプティマイザは、テーブルのサイズが小さい方を「Inner側(ハッシュテーブル側)」に選ぼうとします。しかし、`ANALYZE`をサボっていて統計情報が古いと、オプティマイザがサイズを誤認し、巨大な方をハッシュテーブルにしようとしてメモリがパンクします。
—
実際にどうチューニングするか(コード例)
例えば、こんな注文履歴の集計で考えてみましょう。
— 注文テーブル(orders)と顧客テーブル(customers)を結合
SELECT o.id, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.created_at > ‘2023-01-01’;
もし`orders`が数千万行あるなら、結合キーである`customer_id`にインデックスがあっても、ハッシュ結合が選ばれることが多いです。
このとき、僕ならこんな風に調査します。
— 実行計画を確認
EXPLAIN (ANALYZE, BUFFERS)
SELECT o.id, c.name FROM orders o JOIN customers c ON o.customer_id = c.id;
もし、`Hash Join`のコストが高すぎるなら、以下のステップを試します。
1. 統計情報の更新: `ANALYZE orders; ANALYZE customers;` を実行して、プランナに正しい情報を与える。
2. work_memの調整: 特定の重いクエリだけなら、セッション単位で一時的に増やします。
SET work_mem = ’64MB’;
— ここでクエリを実行
RESET work_mem;
3. 結合順序の工夫: どうしてもハッシュ結合がうまく回らないなら、サブクエリで先に絞り込んでから結合し、ハッシュテーブルのサイズを強制的に小さくするのも賢い戦略です。
—
先輩からのアドバイス
「ハッシュ結合は最強」と思われがちですが、メモリが限られた環境では諸刃の剣です。
- 小規模な結合なら、Nested Loopの方がメモリを食わず速いこともあります。
- 大規模な結合なら、ハッシュ結合が必須です。そのときは`work_mem`の確保を忘れないでください。
データベースは生き物です。EXPLAINの結果を「読める」ようになると、クエリの景色がガラッと変わりますよ。次に重いクエリに遭遇したら、まずは「ハッシュテーブルは綺麗に収まっているか?」という視点で眺めてみてください。
また次回の記事でお会いしましょう!何か詰まったら、いつでも質問してくださいね。
コメント