PostgreSQL 9.6で導入された並列クエリ機能は、バックグラウンドワーカーからリーダープロセスに結果を結合するためのGatherノードを持ち込みました。しかし、標準のGatherノードは並列ワーカーによって生成されたタプルの順序を破壊するため、再帰的なシーケンスを再構築するためにリーダーでの高価な最終Sortステップが必要です。この冗長性を排除するために、バージョン10では、ワーカーからのソートされた入力のk-wayマージを実行し、リーダーサイドのマテリアライゼーションとソートの必要性を回避するGather Mergeノードが導入されました。
プランナーは、並列サブプランが通常Index Scansやタプルシーケンスを保持するMerge Joinsによって生成される必要なプロパティに従って出力を保証する場合にのみ、Gather Mergeを注入します。サブプランがHash Joinsや順序のない集約のような操作で順序を失う場合、Gather Mergeは資格がなくなり、オプティマイザーはコストのかかるSortを伴うGatherと、単一プロセスでの順序を維持するために並列処理を完全に放棄することのいずれかを選択しなければなりません。
サブプランが順序付き出力を保証すると、Gather Mergeはリーダーがマテリアライゼーションとすべてのタプルのソートではなく、最小限のメモリバッファを使用してストリーミングマージを実行できるようにします。メモリ戦略は、リーダーでのソートのために1つの大きな割り当てを使用することから、ソートされた実行を維持するためにワーカーごとに小さなメンテナンスにシフトし、work_memの枯渇とディスクスピルのリスクを大幅に減らします。
私たちのチームは、センサーの読み取り値を含む時系列分析プラットフォームを管理しており、PostgreSQLテーブルは時間ごとにパーティション分けされており、20億行以上を含んでいます。重要なダッシュボードでは、すべてのパーティションからtimestamp降順で最新の1000件の読み取りを表示する必要があり、レイテンシの予算は500ミリ秒未満でした。最初のシングルスレッドクエリプランは、ピーク分析負荷中のユーザーエクスペリエンスにボトルネックを生じさせながら、これらの要件を満たすことができませんでした。
シングルプロセスインデックススキャン: 各パーティションでの逆向きIndex Scanを利用し、その後Limitノードを逐次実行することを最初に検討しました。このアプローチは、複雑な並列調整なしに、実装の簡素さと決定論的な順序を提供しました。ただし、これにより、NVMeストレージアレイのI/O帯域幅を最大限に活用することができず、ピーク負荷中に常に2秒を超えてしまい、リアルタイムダッシュボード更新には不適切でした。
GatherとSortを用いた並列シーケンススキャン: 2つ目のアプローチでは、max_parallel_workers_per_gatherを有効にし、標準のGatherノードを用いたParallel Seq Scanを使用し、すべての行をリーダーに集め、最終的なSortとLimitを実行しました。これによりCPUの並列性が活用され、スキャンのスループットが大幅に向上しました。それにもかかわらず、リーダープロセスは数百万行をソートするために4GB以上のwork_memを割り当て、制約のあるリーダーノードでディスクスピルやOutOfMemoryエラーを頻繁に引き起こし、システムの安定性を損なう結果となりました。
Gather Mergeを用いた並列インデックススキャン: 最終的に、我々はワーカーが降順でParallel Index Scansを実行し、Gather Mergeノードにフィードバックを与えるプランを選択しました。ワーカーは必要な順序でインデックスリーフページをスキャンし、リーダーにソートされたタプルをストリーミングして、軽量のk-wayマージを実行してトップ1000行を抽出しました。このアーキテクチャにより、リーダーでの最終的なソートの必要がなくなり、ストレージ効率を保ちながらメモリ圧力が劇的に減少しました。
我々はGather Mergeアプローチを選択しました。なぜなら、ハッシュベースの操作でそれと戦うのではなく、既存のインデックス構造を活用することによって遅延とメモリ制約の両方を満たしたからです。このソリューションにより、マージバッファのためのリーダーのメモリ使用量が64MB未満に削減され、300ms未満の応答時間を一貫して達成しました。システムは現在、メモリ枯渇なしでピーク負荷を処理でき、並列実行を通じて順序を保持するというアーキテクチャ選択が確認されました。
なぜHash AggregateをGather Mergeノードの下に置くとPostgreSQLのプランナーがプランを拒否するか、明示的なSortステップを挿入するのか、これがGroupAggregateの動作とどのように異なるのか?
Hash Aggregateはタプルをグループ化するために順序のないハッシュテーブルを構築し、その結果、基盤のスキャンによって生成された入力シーケンスが破壊されます。Gather Mergeは、並列ワーカーからのすべての順序付き入力ストリームを必要とし、そのストリーミングk-wayマージを実行するために、集約の順序のない出力は直接利用できなくなります。逆に、GroupAggregateはプレソートされた入力で動作し、GROUP BYキーがソート順序と一致する場合にタプルの順序を保持できるため、英文のGather Mergeとの互換性があり、中間ソートステップが必要ありません。
どのようにparallel_tuple_costGUCが、杖によってソートされたストリームのマージコストを見積もる際に、プランナーがGatherプランからGather Mergeプランに切り替わるしきい値に影響を与えるか?
parallel_tuple_costは、並列ワーカーとリーダープロセス間で行を転送するためのタプルごとのCPUオーバーヘッドを加算します。Gather Mergeの場合、このコストは、マージヒープを維持するために必要な追加の比較ロジックにより、標準のGatherノードよりもわずかに高くなります。推定結果セットが小さい場合、プランナーは、8つのマージストリームの累積オーバーヘッドが小規模なタプルの中心的なソートコストを超える可能性があるため、リーダーでの安価なSortを伴うGatherノードを優先する場合があります。
DECLARE CURSORをSCROLLオプションで、Gather Mergeノードを含むクエリプランを使用する場合に、どの特定の制限が生じ、なぜ実行プログラムがストリーミングの性質にもかかわらず結果セット全体を静かにマテリアライズする可能性があるのか?**
SCROLLカーソルは、結果セットを後方に移動する能力を必要とし、これは行をwork_memにマテリアライズするか、ディスクにスピルして逆方向の取得をサポートする必要があります。Gather Mergeは効率的にストリーミングで順序付きの出力を生成しますが、SCROLLオプションは、実行プログラムにGather Mergeの上にMaterializeノードを挿入させ、潜在的な逆転送のために行をバッファリングさせます。このマテリアライゼーションは、結果セットサイズに比例したメモリを消費し、ストリーミングマージ戦略のメモリ効率の利点を実質的に無効にし、Gather Mergeを最初に選択した場合に避けられるディスクスピルを引き起こす可能性があります。