【実務・中級編】 ハッシュ結合 – PostgreSQL

「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の結果を「読める」ようになると、クエリの景色がガラッと変わりますよ。次に重いクエリに遭遇したら、まずは「ハッシュテーブルは綺麗に収まっているか?」という視点で眺めてみてください。

また次回の記事でお会いしましょう!何か詰まったら、いつでも質問してくださいね。

コメント

タイトルとURLをコピーしました