PostgreSQLの並列クエリ、その「光と影」を語ろう
こんにちは。今日もどこかでクエリの実行計画と格闘しているエンジニアの皆さん、お疲れ様です。
PostgreSQLにおける「Parallel Query(並列クエリ)」が導入された当初、私たちは歓喜しました。重たい集計クエリが、CPUコアの数だけ加速する。かつては数分かかっていたフルスキャンが、数秒で終わるようになった時のあの感動は、今でも忘れられません。
しかし、現場で多くのシステムをチューニングしていると、並列クエリは単なる「魔法の杖」ではないことに気づかされます。「とりあえず並列数を増やせば速くなる」という幻想を抱いていると、本番環境で思わぬ落とし穴にハマるものです。
今日は、PostgreSQLの内部アーキテクチャの視点から、並列クエリの深淵を少し覗いてみましょう。
—
並列クエリのアーキテクチャを理解する
PostgreSQLの並列実行は、Leaderプロセスと複数のWorkerプロセスによる協調作業で成り立っています。
Leaderプロセスは、クエリを受信し、実行計画を立て、結果をクライアントに返す司令塔です。一方でWorkerプロセスは、Leaderから受け取った計画を黙々と実行し、その結果を「共有メモリ(Parallel Message Queue)」を通じてLeaderに吸い上げさせます。
ここで重要なのは、「並列化されるのはクエリの実行だけではない」という点です。メモリの割り当て、プロセス間の同期コスト、そして何より「コンテキストスイッチ」という隠れたコストが発生しています。
なぜ「並列数」を増やせばいいわけではないのか
よくあるミスが、`max_parallel_workers_per_gather`をむやみに引き上げることです。
- 共有メモリの枯渇: `dynamic_shared_memory_type`の制限や、`max_parallel_workers`を超えた割り当ては、クエリを並列化しようとして逆にオーバーヘッドを増大させます。
- I/Oのボトルネック: そもそもデータがストレージ上にバラバラに配置されている場合、CPUを増やしてもI/O待ちが増えるだけで、結局は「待機時間の合計」は変わりません。
- ロック競合: 複数のワーカーが同じテーブルの同じデータページにアクセスしようとすれば、当然ながら競合が発生します。特にWriteが多い環境では、並列クエリはかえって毒になることもあります。
—
パフォーマンストラブルシューティングの勘所
もし本番環境で「並列クエリが動いているはずなのに遅い」と感じたら、まずは`EXPLAIN (ANALYZE, VERBOSE)`を叩くのが第一歩です。しかし、真のエンジニアはそこから先を見ます。
1. 「期待値」と「現実」の乖離を見る
`EXPLAIN ANALYZE`の出力で注目すべきは、「Workers Planned」と「Workers Launched」の差です。もしPlannedが2なのにLaunchedが0、あるいは1であれば、並列化のためのリソース制限(`max_worker_processes`や`max_parallel_maintenance_workers`)に触れているか、コスト閾値(`min_parallel_table_scan_size`等)の調整が適切ではありません。
2. 共有メモリの待ち時間(Wait Events)を確認する
`pg_stat_activity`を見てみてください。もしワーカーが`ParallelMessageWait`や`DataQueue`のイベントで長時間待機しているなら、それはLeaderプロセスが結果を回収しきれていないか、あるいはワーカー間でデータ転送の帯域が飽和しているサインです。
3. ハッシュ結合のオーバーヘッド
大規模な結合を行う際、`Parallel Hash Join`は強力ですが、ハッシュテーブルを共有メモリ上に構築するコストを忘れてはいけません。メモリ不足でディスクに溢れた瞬間、並列クエリの恩恵は霧散します。`work_mem`の設定は、ワーカーの数だけでなく、各ワーカーが個別にどれだけのメモリを食うかを計算して決める必要があります。
—
私たちの「作法」
並列クエリを使いこなすために、私が心掛けているルールをいくつか共有します。
- OLTPとOLAPを分ける: 同時実行数が多いOLTP環境では、並列クエリは慎重に設定します。むしろ、`max_parallel_workers_per_gather`を控えめにして、クエリごとの応答速度よりもシステム全体の安定性を優先させることもあります。
- コストパラメータの微調整: `random_page_cost`や`seq_page_cost`がデフォルトのままだと、PostgreSQLは並列化の判断を誤ることがあります。SSD環境であれば、`random_page_cost`を1.1程度に下げるだけで、実行計画が劇的に改善されるケースは非常に多いです。
- インデックスの最適化: 並列クエリに頼りすぎる前に、そもそも並列フルスキャンが必要ないデータ構造になっていないかを見直す。「並列クエリで解決」は最後の手段と考えるのが、熟練者の矜持かもしれません。
—
最後に
並列クエリは、PostgreSQLが持つ強力な武器です。しかし、その武器を活かすも殺すも、データベースの特性とハードウェアの限界をどこまで理解しているかに懸かっています。
「なぜ並列化が発動しないのか?」「なぜ並列化しているのに遅いのか?」という問いに対して、内部構造という裏側のロジックで答えが出せた時、エンジニアとしての視界が一段階開けるはずです。
皆さんのクエリが、今日も軽快に走ることを願っています。それでは、また。
コメント