PostgreSQL 18 中文参考文档(未翻译部分保留英文)🌐 切换到英文原版 →

15.3. 并行执行计划 #

15.3.1. 并行扫描
15.3.2. 并行连接
15.3.3. 并行聚合
15.3.4. 并行追加
15.3.5. 并行计划调优建议

因为每个参与协同的 worker 进程都会将其分得的计划段落完整执行至终点,因此系统不能简单粗暴地将一份普通串行执行计划直接复制分发给多个 worker(那样做会导致每个进程都重复扫描全表并产出一份全量结果集副本,不仅没有性能提升,还会使最终返回的数据出现严重重复)。在内部实现上,能够并发执行的子计划必须被构造为部分执行计划(partial plan):它必须精细设计,确保每个协同进程在各自扫描时仅产出目标结果集的一个互斥子集,且所有进程产出的子集恰好完全覆盖、无一遗漏且绝不重复。这通常要求查询的驱动表扫描算子必须是感知并发的专用算子。

15.3.1. 并行扫描 #

系统目前支持以下感知并发的物理表扫描类型:

  • 并行顺序扫描(Parallel Sequential Scan):数据表的物理数据块被划分为动态范围区间并放入共享协调状态池中。各个工作进程按需动态向协调器认领一组数据块范围并执行连续扫描,处理完毕后再次申请下一组,直到全表扫描完成。

  • 并行位图堆扫描(Parallel Bitmap Heap Scan):由 leader 进程先行扫描索引构建表示命中物理块分布的内存位图,随后将待访问的物理数据块列表分摊给各并发 worker 进程共同执行底层堆表数据扫描(即索引扫描串行,而底层回表扫描并行)。

  • 并行索引扫描(Parallel Index Scan)与并行仅索引扫描(Parallel Index-Only Scan):各个协同进程轮流从 B 树索引中安全并发提取数据页。每个进程认领单个索引物理块,并扫描提取该块引用的所有元组。B 树并行扫描保证在各个 worker 内部产出的局部数据严格按序排列。

15.3.2. 并行连接 #

类似于普通串行计划,驱动表可以通过嵌套循环连接(Nested Loop)、哈希连接(Hash Join)或合并连接(Merge Join)与其他表进行关联:

  • 在嵌套循环连接中,内表扫描通常是非并行的。若内表驱动的是索引扫描,则执行效率极高,因为外表的驱动元组被平均分发给了各个并发 worker,多进程各自并发执行针对内表索引的探针查找。

  • 在普通哈希连接中,若未加并行前缀,内表必须被每个 worker 进程各自全量扫描一遍,在各自本地内存中构建完全相同哈希表的副本,这在大数据集下开销昂贵。而在并行哈希连接(Parallel Hash Join)中,内表被作为共享并发哈希表构建,各协同进程共同分担构建共享哈希表的物理开销。

15.3.3. 并行聚合 #

PostgreSQL 采用成熟的两阶段模式实现并行聚合:第一阶段,参与并行计算的每个 worker 进程各自对本地扫描的数据执行初级聚合,产出局部中间汇总结果,这在计划树中表现为 Partial Aggregate 算子;第二阶段,各 worker 产出的局部中间结果通过 Gather 收集回传至主 leader 进程;最后,由 leader 进程对所有分片的局部结果执行最终合并二次汇总,这表现为 Finalize Aggregate 节点。

由于最终聚合合并发生在主进程单核上,若查询分组产出的唯一分组键数量极大(几乎接近输入总行数),并行聚合收益将大打折扣,优化器在代价估算时会智能识别该特征并倾向于回退串行方案。

15.3.4. 并行追加 #

当需要合并来自多个独立数据源的行集时(例如执行 UNION ALL 或扫描分区表的各个子分区),计划树会引入 Append 节点。在并行计划中,优化器可以调度引入更先进的 Parallel Append 算子:各并发 worker 会被尽可能均匀地分散交错分派给不同的子分区或子查询并发推进,消除对单分区的争用排队瓶颈。

15.3.5. 并行计划调优建议 #

如果期望走并行的复杂长查询未生成并行计划,可以尝试在测试环境中适当调小启动代价阈值参数 parallel_setup_cost 或单行元组传递开销参数 parallel_tuple_cost。在排查并行性能时,推荐使用 EXPLAIN (ANALYZE, VERBOSE),它会打印出各个 worker 进程在各个计划算子节点上的实际执行耗时与扫描行数分布,直观呈现负载倾斜状况。