【テクニカル・上級編】 ハッシュ結合 – PostgreSQL

ハッシュ結合の深淵:PostgreSQLの内部で何が起きているのか?

データベースエンジニアとして長く現場にいると、クエリプランナーが吐き出す「Hash Join」の文字に、ある種の安心感と、同時に一抹の不安を覚えるようになります。

「ハッシュ結合は速い」。これは定説ですが、なぜ速いのか、そして、なぜ突然牙を剥くのか。その境界線を知っているかどうかで、パフォーマンスチューニングの質は劇的に変わります。今日は、PostgreSQLの内部アーキテクチャの視点から、この「静かなる加速装置」を解剖してみましょう。

—

1. ビルドとプローブ:2つのフェーズの静かなるダンス

PostgreSQLのハッシュ結合は、大きく分けて2つのフェーズで構成されています。

1. ビルドフェーズ (Build Phase): 結合対象の一方(通常は小さい方)をスキャンし、インメモリのハッシュテーブルを構築する。
2. プローブフェーズ (Probe Phase): もう一方のテーブルをスキャンしながら、ハッシュテーブルをルックアップしてマッチする行を取り出す。

ここで重要なのは、「ビルドされる側がいかにメモリ(`work_mem`)に収まるか」という点です。もし溢れ出せば、PostgreSQLは容赦なく「Disk-based Hash Join」へと移行します。これは、一時ファイル(`pg_stw`)への書き出しと読み込みを伴う、パフォーマンス上の暗黒時代への入り口です。

2. なぜ「メモリ不足」は悪夢なのか

`work_mem` を超えた瞬間、PostgreSQLはデータをバケット単位でディスクに書き出します。このとき発生するI/Oコストもさることながら、怖いのは「バケットの偏り」です。

ハッシュ関数の選択が不運にも特定のバケットにデータを集中させてしまうと、たとえ全体としてはメモリ容量内であっても、そのバケットだけがディスクへ追い出されることになります。これを防ぐためにPostgreSQLは動的な再ハッシュ化を行いますが、これには多大なCPUコストがかかります。

現場で「なぜか特定のクエリだけCPUが跳ね上がる」というケースに遭遇したら、まず `EXPLAIN (ANALYZE, BUFFERS)` を見てください。`Batches` の値が1よりも大きくなっていたら、それが悲鳴の正体です。

3. 実践的トラブルシューティングの勘所

ハッシュ結合を使いこなすための、私なりの「チェックリスト」をいくつか共有します。

  • プランナーの甘い期待を裏切るな:

PostgreSQLのプランナーは、統計情報に基づいてどちらを「ビルド側」にするか決めます。もし統計情報が古ければ、プランナーは「小さいはずのテーブル」を誤認し、巨大なテーブルをメモリに乗せようとして破綻します。`ANALYZE` の頻度や統計目標値(`STATISTICS`)の見直しは、ハッシュ結合の安定化に直結します。

  • `work_mem` の誘惑に負けない:

「全部メモリに乗れば速いんだから、`work_mem` を大きくすればいい」という考えは危険です。同時実行数が多いシステムでそれをやると、メモリ不足によるOOM Killerの餌食になります。接続ごとに消費されるメモリであることを忘れず、セッション単位での一時的な引き上げ(`SET LOCAL work_mem = ‘…’;`)を検討するのがプロの作法です。

  • ハッシュキーのカーディナリティ:

結合キーが極端に偏っている場合、ハッシュテーブルのルックアップ効率が低下します。インデックスが効かない結合であればあるほど、ハッシュ結合は「ハッシュの衝突」という別の戦いに突入します。

結びに:魔法ではない、計算である

ハッシュ結合は万能ではありません。小規模なデータであれば「Nested Loop」の方が速いこともありますし、データがソート済みであれば「Merge Join」が圧倒的な効率を誇ります。

しかし、大規模なデータセットを扱うとき、このハッシュ結合こそが「計算量O(N+M)」という強力な武器になります。内部で何が起きているのか、メモリのどこでデータが踊っているのかを想像できるようになると、SQLの書き方は間違いなく変わります。

皆さんのクエリが、今日も効率的にメモリ上を駆け巡ることを願っています。

—
何か特定の実行計画で詰まっていることがあれば、ぜひコメントで教えてください。一緒にプランを読み解きましょう。

コメント

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