由于每个工作进程都会将计划的并行部分执行到底,因此不能简单地拿一个普通查询计划并让多个工作进程同时运行。那样每个工作进程都会生成完整输出结果集的一份副本,所以查询不仅不会比平常更快,反而会产生错误结果。相反,计划的并行部分必须是查询优化器内部所说的部分计划;也就是说,它必须被构造为使执行该计划的每个进程只生成输出行的一个子集,并且保证每一条所需输出行都恰好由某个协作进程生成一次。一般来说,这意味着查询驱动表上的扫描必须是并行感知扫描。
目前支持下列类型的并行感知表扫描。
在并行顺序扫描中,表的块会被划分为多个范围,并在协作进程之间共享。 每个工作进程都会先完成分配给自己的块范围扫描,然后才会请求额外的块范围。
在并行位图堆扫描中,会选择一个进程作为领导者。 该进程执行一个或多个索引扫描,并构建一个位图,以指示需要访问哪些表块。 然后这些块会像并行顺序扫描那样分配给协作进程。 换句话说,堆扫描是并行执行的,但底层索引扫描不是。
在并行索引扫描或并行仅索引扫描中, 协作进程轮流从索引中读取数据。 目前,并行索引扫描仅支持 B-树索引。 每个进程都会认领一个索引块,并扫描并返回该块所引用的全部元组; 与此同时,其他进程可以从不同的索引块返回元组。 并行 B-树扫描的结果在每个工作进程内部都会按排序顺序返回。
其他扫描类型,例如非 B-树索引扫描,将来也可能支持并行扫描。
与非并行计划一样,驱动表可以通过嵌套循环、哈希连接或归并连接与一个或多个其他表连接。连接的内侧可以是规划器支持的任何类型的非并行计划,只要它能够安全地在并行工作进程中运行。根据连接类型,内侧也可以是并行计划。
在嵌套循环连接中,内侧始终是非并行的。尽管它会被完整执行,但如果内侧是索引扫描,这种方式仍然是高效的,因为外侧元组以及在索引中查找值的循环会分摊到多个协作进程上。
在归并连接中,内侧始终是非并行计划,因此会被完整执行。这可能效率不高,尤其是在必须执行排序时,因为工作量和结果数据会在每个协作进程中重复。
在哈希连接(没有“并行”前缀)中,每个协作进程都会完整执行内侧,以构建同样的哈希表副本。如果哈希表很大,或者该计划开销很高,这种方式可能效率不高。在并行哈希连接中,内侧是一个并行哈希,它会将构建共享哈希表的工作分摊到多个协作进程上。
PostgreSQL 通过分两个阶段进行聚合来支持并行聚合。首先,每个参与查询并行部分的进程执行一个聚合步骤,为该进程所见到的每个分组产生一个部分结果。这在计划中体现为一个 Partial Aggregate 节点。然后,部分结果通过 Gather 或 Gather Merge 传送给领导者。最后,领导者会把来自所有工作进程的结果再次聚合,以产生最终结果。这在计划中体现为一个 Finalize Aggregate 节点。
由于 Finalize Aggregate 节点运行在领导者进程上,因此对于那些相对于输入行数会产生较多分组的查询,查询规划器会认为它不太有利。例如,在最坏情况下,Finalize Aggregate 节点看到的分组数可能与所有工作进程在 Partial Aggregate 阶段看到的输入行数一样多。对于这种情况,使用并行聚合显然不会带来性能收益。查询规划器会在规划过程中考虑这一点,因此在这种场景下不太可能选择并行聚合。
并行聚合并非在所有情况下都受支持。每个聚合都必须是并行安全的,并且必须具有组合函数。如果该聚合具有类型为 internal 的转移状态,那么它还必须具有序列化和反序列化函数。更多细节请参见 CREATE AGGREGATE。如果任何聚合函数调用包含 DISTINCT 或 ORDER BY 子句,则不支持并行聚合。对于有序集聚合,或者当查询涉及 GROUPING SETS 时,也不支持并行聚合。只有当查询涉及的所有连接也都属于计划并行部分时,才能使用并行聚合。
每当 PostgreSQL 需要将来自多个源的行合并成一个结果集时,它就会使用 Append 或 MergeAppend 计划节点。这种情况常见于实现 UNION ALL 或扫描分区表时。这样的节点和其他任何计划中的情形一样,也可以用于并行计划。不过,在并行计划中,规划器也可能改用 Parallel Append 节点。
当 Append 节点用于并行计划时,每个进程都会按照子计划出现的顺序执行它们,因此所有参与进程会协作执行第一个子计划直到其完成,然后再大致同时转向第二个子计划。而当使用 Parallel Append 时,执行器会尽可能均匀地将参与进程分散到各个子计划上,从而使多个子计划能够同时执行。这样既避免了争用,也避免让那些从未执行某个子计划的进程承担其启动代价。
此外,普通的 Append 节点在并行计划中只能拥有部分子计划,而 Parallel Append 节点则既可以拥有部分子计划,也可以拥有非部分子计划。非部分子计划只能由单个进程扫描,因为重复扫描会产生重复结果。因此,即使没有高效的部分计划可用,涉及追加多个结果集的计划仍然可以实现粗粒度并行。比如,考虑一个针对分区表的查询,而该查询只有通过某个不支持并行扫描的索引才能高效实现。规划器可能会选择由普通 Index Scan 计划组成的 Parallel Append;每个单独的索引扫描仍然必须由单个进程执行到底,但不同的扫描可以由不同进程同时执行。
enable_parallel_append 可用于禁用这一特性。
如果一个本来预期会生成并行计划的查询却没有生成并行计划,可以尝试降低 parallel_setup_cost 或 parallel_tuple_cost。当然,这样得到的计划可能会比规划器原本偏好的串行计划更慢,但并不总是如此。如果即使把这些设置调得很低(例如都设为零)之后仍然得不到并行计划,那么可能存在某些原因使查询规划器无法为该查询生成并行计划。关于可能的原因,请参见 Section 15.2 和 Section 15.4。
在执行并行计划时,可以使用 EXPLAIN (ANALYZE, VERBOSE) 显示每个计划节点的逐工作进程统计信息。这有助于判断工作是否在各个计划节点之间均匀分布,以及更全面地理解该计划的性能特征。
如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。