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

「先輩、最近クエリが遅いんですけど、実行計画を見たら『Hash Join』のところで『Batches』って表示されていて……」

先日、後輩からこんな相談を受けました。PostgreSQLを触っていると一度はぶつかる壁、そう、「メモリ不足によるディスク溢れ(Spill to disk)」ですね。

今日は、PostgreSQLのパフォーマンスチューニングにおける「隠れた主役」、`work_mem` とハッシュ結合の深掘り話をしましょう。教科書的な説明はドキュメントに譲るとして、現場でどう立ち回るべきか、僕なりの視点で解説します。

—

なぜ「ハッシュ結合」はメモリを食うのか?

PostgreSQLが2つのテーブルを結合するとき、ハッシュ結合は非常に強力な武器になります。片方のテーブル(通常は小さい方)をメモリ上にハッシュテーブルとして展開し、もう片方のテーブルをスキャンしながら突き合わせる。これで計算量はほぼ線形に収まります。

しかし、この「メモリ上に展開する」というプロセスが曲者なんです。ここで使われる作業用メモリの制限値が、おなじみの `work_mem` です。

もし、ハッシュテーブルのサイズが `work_mem` を超えてしまったらどうなるか? PostgreSQLは泣く泣くハッシュテーブルを分割し、一部を一時ファイル(ディスク)に書き出します。これが「ディスクへの溢れ(Spill to disk)」です。HDDなら言うまでもなく、高速なNVMe SSDであっても、メモリ上のアクセスとは比較にならないほど遅い。これが、クエリが突然重くなる正体です。

「Batches」というサインを見逃すな

`EXPLAIN ANALYZE` を実行したとき、こんな表示を見たことはありませんか?

Hash Join (cost=… rows=… width=…)
Hash Cond: (a.id = b.a_id)
Batches: 5 Memory Usage: 4096kB Disk: 10240kB

この「Batches」が1より大きい場合、それはメモリに乗り切らず、データを分割して処理したという証拠です。パフォーマンスを最大化したいなら、理想は Batches: 1 です。

work_memを上げればいい……わけじゃない罠

「じゃあ、`work_mem` を大きくすればいいじゃん!」と考えるのは自然です。でも、ここで注意が必要。

`work_mem` はクエリ内の各ノードが消費できるメモリ量です。さらに恐ろしいのは、1つのクエリの中で並列実行や複数の結合が行われると、その数だけ `work_mem` が消費されるということ。

例えば、`work_mem` を 256MB に設定したとします。もしそのクエリがパラレルクエリ(`max_parallel_workers_per_gather = 4`)で動いたら、それだけで 1GB 以上のメモリを一瞬で食いつぶす可能性があります。同時実行数が多いWebアプリケーションでこれをやると、あっという間にOSのメモリを枯渇させ、OOM Killerにプロセスを強制終了される……という悪夢が待っています。

僕が現場で行う「実践的チューニング」

僕が現場でチューニングをする際の手順はこんな感じです。

1. グローバル設定は「控えめ」に

`postgresql.conf` での `work_mem` は、全接続の合計が物理メモリを超えない程度に留めます(例: 4MB〜16MB程度)。

2. 重いクエリだけ「ピンポイント」で増やす

これが一番安全です。特定のバッチ処理や、どうしても遅い解析用クエリに対してのみ、セッション単位で拡張します。

— トランザクション内で一時的に引き上げる
BEGIN;
SET LOCAL work_mem = ‘128MB’;

SELECT FROM large_table_a
JOIN large_table_b ON a.id = b.a_id;

COMMIT;

3. まずは「効率」を疑う

メモリを増やす前に、そもそもそのハッシュ結合を小さくできないか考えます。

  • インデックスは適切か?(不要なカラムをSELECTしていないか)
  • フィルタリングは先行しているか?(WHERE句で十分に絞り込めているか)
  • 統計情報は最新か?(`ANALYZE` を忘れていないか)

PostgreSQLがテーブルのサイズを見誤っているせいで、ハッシュ結合を選んでいるだけというケースも多々あります。

最後に:エンジニアとしての勘所

結局のところ、データベースのチューニングは「リソースの奪い合い」をどう交通整理するかに集約されます。

`work_mem` を増やすのは、例えるなら「作業机を広くする」ようなもの。机が広いと仕事は捗りますが、全員の机を広げすぎるとオフィスがパンクしますよね。だからこそ、本当に広いスペースが必要な特定のプロジェクトの時だけ、一時的に広い会議室(セッション)を使うのが、プロのエンジニアの作法です。

皆さんも、実行計画の `Batches` を眺めながら、メモリとディスクの境界線で遊んでみてください。きっと、PostgreSQLの挙動が今までより少しだけ愛おしくなるはずですよ。

それでは、また現場で!

コメント

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