因为每个工作者只执行完成计划的并行部分,所以不可能简单地产生一个普通查询计划并使用多个工作者运行它。每个工作者都会产生输出结果集的一个完全拷贝,因而查询并不会比普通查询运行得更快甚至还会产生不正确的结果。相反,计划的并行部分一定被查询优化器在内部当作一个部分计划,即它必须被构建出来,这样每一个执行该计划的进程将以无重复地方式产生输出行的一个子集,即保证每一个所需要的输出行正好只被一个合作进程生成。通常,这意味着该查询的驱动表上的扫描必须是一种可并行的扫描。
目前支持以下类型的并行感知表扫描。
在并行顺序扫描中,表的块将被分成范围并在合作进程之间共享。 每个工作进程将在请求额外的块范围之前完成其给定范围的扫描。
在并行位图堆扫描中,选择一个进程作为领导者。 该进程执行一个或多个索引的扫描,并构建一个位图,指示需要访问哪些表块。 然后将这些块分配给合作进程,就像在并行顺序扫描中一样。 换句话说,堆扫描是并行执行的,但底层索引扫描不是。
在并行索引扫描或并行索引唯一扫描中, 合作进程轮流从索引中读取数据。 目前,仅支持btree索引的并行索引扫描。 每个进程将声明一个单个索引块,并扫描并返回该块引用的所有元组; 其他进程同时可以从不同的索引块返回元组。 并行btree扫描的结果将在每个工作进程内按排序顺序返回。
其他扫描类型,如非btree索引的扫描,可能在将来支持并行扫描。
正如在非并行计划中那样,驱动表可能被使用嵌套循环、哈希连接或者归并连接连接到一个或者多个其他表。连接的内侧可以是任何类型的被规划器支持的非并行计划,假设它能够安全地在并行工作者中运行。根据连接类型,内侧还可以是一种并行计划。
在一个嵌套循环连接中,内侧总是非并行的。尽管它会被完全执行,如果内侧是一个索引扫描也会很高效,因为外侧元组以及在索引中查找值的循环会被划分到多个合作进程。
在一个归并连接中,内侧总是一个非并行计划并且因此会被完全执行。这可能是不太高效的,特别是在排序必须被执行时,因为在每一个合作进程中工作数据和结果数据是重复的。
在一个哈希连接(没有“并行”前缀)中,每个合作进程都会完全执行内侧以构建哈希表的相同拷贝。如果哈希表很大或者该计划开销很大,这种方式就很低效。在一个并行哈希连接中,内侧是一个并行哈希,它把构建共享哈希表的工作划分到多个合作进程。
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。当然,这个计划可能比规划器优先产生的顺序计划还要慢,但也不总是如此。如果将这些设置为很小的值(例如把它们设置为零)也不能得到并行计划,那就可能是有某种原因导致查询规划器无法为你的查询产生并行计划。可能的原因可见第 15.2 节和第 15.4 节。
在执行一个并行计划时,可以用EXPLAIN (ANALYZE,VERBOSE)
来显示每个计划节点在每个工作者上的统计信息。这些信息有助于确定是否所有的工作被均匀地分发到所有计划节点以及从总体上理解计划的性能特点。