PostgreSQLの「並列クエリ」を解剖する:Gatherとワーカーの裏側
PostgreSQLでクエリが思ったように速くならない時、あるいは実行計画(`EXPLAIN`)に潜む `Gather` ノードを眺めながら「なぜここで並列数が足りないのか」と頭を抱えたことはないだろうか。
今回は、PostgreSQLの並列クエリ(Parallel Query)の心臓部、特に `Gather` と `Gather Merge`、そしてバックグラウンドワーカーがどのように連携しているのか、その「内側の景色」について深掘りしてみたい。教科書的な概念説明はあえて飛ばし、現場でトラブルを解決するための解像度を高める話をしよう。
—
1. GatherとGather Merge:並列世界の「収束点」
まず押さえておきたいのは、PostgreSQLの並列処理は「分散」ではなく「ローカルでの並列計算」であるという点だ。
`Gather` ノードは、パラレルワーカーたちが並列でスキャンや結合を行った結果を、リーダープロセス(クエリを発行した大元のプロセス)に集約するゲートウェイだ。ここで重要なのは、「集約されるときにデータがソートされているか否か」である。
- Gather: ワーカーからの結果を単に非同期に受け取る。順序は保証されない。
- Gather Merge: ワーカーごとに既にソート済みのデータ(例えば `Index Scan` の結果など)を、マージソートの要領で順序を保ったまま統合する。
もし、期待していたはずの `Gather Merge` ではなく `Gather` が選ばれているなら、それはプランナが「ソート順を維持するコスト」を過大評価しているか、あるいは統計情報が不正確で、そもそもソート済みであることを検知できていない可能性が高い。
—
2. バックグラウンドワーカーの「影の立ち回り」
PostgreSQLの並列クエリでは、`max_parallel_workers_per_gather` で設定した数のワーカーが、共有メモリ(`Parallel Context`)を介してリーダーと連携する。
ここで陥りがちなのが、「ワーカーが暇をしているのにクエリが遅い」というケースだ。これを読み解く鍵は、ワーカーの起動コストと、メモリの「おすそ分け」にある。
なぜワーカーは本気を出さないのか?
1. Work Memの分断: 1つの大きな `work_mem` を確保するのではなく、並列化されたクエリでは、各ワーカーが個別に `work_mem` を消費する。合計メモリが `work_mem` の制限に抵触しそうになると、プランナは並列度を下げてしまう。
2. リーダーのボトルネック: `Gather` ノードにおいて、リーダープロセスがワーカーからのデータを受け取り、それをクライアントに流す「シリアル処理」がボトルネックになることが多々ある。ワーカーがどれだけ爆速でスキャンしても、リーダーがボトルネックなら並列化の恩恵は頭打ちだ。
—
3. 実践:現場でのパフォーマンストラブルシューティング
もし君の目の前で「並列クエリが動いているはずなのに遅い」というクエリがあるなら、まずは以下の3点をチェックしてみてほしい。
- 「Parallel Aware」なスキャンをしているか?
`EXPLAIN ANALYZE` を見たとき、`Parallel Seq Scan` なのか単なる `Seq Scan` なのか。もし単なる `Seq Scan` が並列クエリの一部として動いているなら、それはワーカーが「共有バッファの読み取り」を効率的に行えていない可能性がある。
- 共有メモリの枯渇: `pg_stat_activity` で、ワーカーが `wait_event_type` として `IPC` 関連の待機をしていないか確認しよう。ワーカー同士の同期やバッファのロック競合が起きている場合、並列化はかえって毒になる。
- データスキュー: 特定のワーカーだけが重いデータのスキャンを担っていないか。`pg_stat_progress_parallel_query` ビューを覗くと、各ワーカーの進捗が手に取るようにわかる。ここで偏りがあるなら、パーティショニングやインデックス設計の見直しが急務だ。
—
最後に:並列クエリは「魔法」ではない
PostgreSQLの並列クエリは、近年のアップデートで非常に強力になった。しかし、それはあくまで「計算資源を贅沢に使う」というトレードオフの上に成り立っている。
OLTP(トランザクション処理)が混在する環境で、むやみに `max_parallel_workers` を上げれば、システム全体がメモリ不足で `OOM Killer` の餌食になるか、ディスクI/Oの飽和を引き起こす。
僕たちがエンジニアとしてやるべきことは、`Gather` ノードにすべてを任せるのではなく、適切なインデックスと統計情報で、プランナが「迷わず最適な並列計画を選択できる」ような舞台を整えてやることだ。
クエリチューニングは、データベースとの対話だ。ログの中に隠れたワーカーの息遣いを感じ取れるようになれば、君もPostgreSQLの深淵を覗く一人になれるはずだ。
次は、並列クエリにおける `CTE` の最適化について話そうか。あれはまた、別の地獄が待っているからね。
コメント