SQL编程高级数据库工程师

在查询优化阶段,**PostgreSQL** 的规划器在何种精确阈值下确定 **Gather Merge** 节点比 **Gather** 节点更适合进行并行结果合并,且底层扫描节点的哪一特定属性决定了这个选择?

用 Hintsage AI 助手通过面试

对于问题的回答

PostgreSQL 9.6 中引入的并行查询功能让 Gather 节点能够将后台工作程序的结果合并到领导进程中。然而,标准的 Gather 节点会破坏并行工作程序生成的任何元组顺序,导致领导端需要一个昂贵的最终 Sort 步骤来重新建立顺序。为了解决在处理固有有序数据流时的冗余,10 版引入了 Gather Merge 节点,它对来自工作的有序输入进行 k-路合并,避免了领导端的材料化和排序的需要。

只有当并行子计划保证输出按照所需属性有序时,规划器才会选择注入 Gather Merge,该属性通常由保持元组顺序的 Index ScansMerge Joins 生成。如果子计划通过 Hash Joins 或无序聚合等操作失去顺序,则 Gather Merge 将不合格,迫使优化器在需要保持顺序的情况下在 Gather 后跟一个代价高昂的 Sort 或完全放弃并行处理。

当子计划保证有序输出时,Gather Merge 允许领导以最小的内存缓冲区执行流式合并,而不是材料化和排序所有元组。内存策略从在领导中进行一次大的排序分配转变为每个工作程序小规模维护有序运行,显著降低了 work_mem 耗尽和大规模有序检索期间磁盘溢出的风险。

生活中的情况

我们的团队管理了一个时间序列分析平台,该平台将传感器读数存储在一个按小时分区的 PostgreSQL 表中,包含超过 20 亿行。一款关键的仪表板需要显示所有分区中最新的 1000 条读数,按 timestamp 降序排列,延迟预算低于 500 毫秒。最初的单线程查询计划未能满足这些要求,在高峰分析负载期间造成用户体验瓶颈。

单进程索引扫描:我们最初考虑在每个分区上使用反向 Index Scan,随后执行一个 Limit 节点进行顺序处理。此方法提供了实现简单性和确定性排序,而无需复杂的并行协调。然而,它未能充分利用我们 NVMe 存储阵列的 I/O 带宽,且在高负载期间始终超过 2 秒,无法接受实时仪表板更新。

并行序列扫描与聚集和排序:第二种方法涉及启用 max_parallel_workers_per_gather 并使用带有标准 Gather 节点的 Parallel Seq Scan,将所有行收集到领导端进行最终的 SortLimit。这利用了 CPU 并行性,大幅提高了扫描吞吐量。然而,它使得领导进程为排序数百万行分配了超过 4GB 的 work_mem,频繁触发磁盘溢出和 OutOfMemory 错误,这对我们资源受限的领导节点的系统稳定性产生了影响。

并行索引扫描与聚集合并:我们最终选择了一个计划,让工作人员以降序时间戳顺序执行 Parallel Index Scans,并通过 Gather Merge 节点进行汇集。工作人员扫描索引叶页按所需顺序流式传送有序元组给领导,领导执行轻量级的 k-路合并以提取前 1000 行。此架构消除了领导的最终排序需要,显著降低内存压力,同时保持流式效率。

我们选择 Gather Merge 方法,因为它独特地满足了延迟和内存约束,通过利用现有的索引结构,而不是通过基于哈希的操作进行争斗。该解决方案将领导的内存占用降至 64MB 以下,用于合并缓冲,并实现了一致的低于 300 毫秒的响应时间。系统现在能够处理峰值负载而不耗尽内存,验证了通过并行执行保持顺序的架构选择。

候选人常常忽视的内容

为什么在 Gather Merge 节点下放置一个 Hash Aggregate 会导致 PostgreSQL 规划器拒绝该计划或插入一个显式的 Sort 步骤,这与 GroupAggregate 的行为有何不同?

Hash Aggregate 构建一个无序哈希表来分组元组,这从根本上破坏了底层扫描生成的任何输入顺序。由于 Gather Merge 需要所有并行工作程序的严格有序输入流以执行其流式 k-路合并,聚合的无序输出阻碍了其直接使用。相反,GroupAggregate 可以在预先排序的输入上运行,并在 GROUP BY 键匹配排序顺序时保持元组排序,从而使其与 Gather Merge 兼容,而无需中间排序步骤。

如何通过 parallel_tuple_cost GUC 影响规划器在评估来自八个并行工作程序的排序流合并成本时,从 Gather 计划切换到 Gather Merge 计划的阈值?

parallel_tuple_cost 为在并行工作程序与领导进程之间传输行增加了每个元组的 CPU 开销。对于 Gather Merge,由于需要维持合并堆的附加比较逻辑,这个成本略高于标准的 Gather 节点。当估计的结果集较小时,规划器可能会更倾向于选择一个带有领导端便宜 SortGather 节点,而不是 Gather Merge,因为八个合并流的累计开销可能超过在中央对少量元组进行排序的成本。

使用 DECLARE CURSORSCROLL 选项时,围绕含有 Gather Merge 节点的查询计划会出现什么具体限制,尽管合并是流式的,为什么执行器可能会默默材料化整个结果集?

SCROLL 游标需要能够反向移动通过结果集,这需要在 work_mem 中材料化行或溢出到磁盘以支持向后抓取。尽管 Gather Merge 高效地产生流式、有序输出,SCROLL 选项强迫执行器在 Gather Merge 上方插入一个 Materialize 节点,以缓冲潜在的反向遍历行。此材料化消耗与结果集大小成比例的内存,有效地抵消了流式合并策略的内存效率优势,可能导致与最初选择 Gather Merge 避免的磁盘溢出等同的原因。