因为每个工作者只执行完成计划的并行部分,所以不可能简单地产生一个普通查询计划并使用多个工作者运行它。每个工作者都会产生输出结果集的一个完全拷贝,因而查询并不会比普通查询运行得更快甚至还会产生不正确的结果。相反,计划的并行部分一定被查询优化器在内部当作一个部分计划。也就是说,一定要这样来创建计划,使得每个将执行该计划的进程只产生输出行的一个子集,这样可以保证每个需要被输出的行刚好会被合作进程产生一次。 通常,这意味着查询驱动表上的扫描必须是并行感知扫描。
目前支持以下类型的并行感知表扫描。
在并行顺序扫描中,表格的块将在协作过程中分配。 块一次发放一个,所以访问表仍然是连续的。
在一个并行位图堆扫描中,选择一个进程作为领导者。 该进程执行一个或多个索引的扫描并构建指示需要访问哪些表块的位图。 然后这些块在并行顺序扫描中在协作进程中分配。换句话说, 堆扫描是并行执行的,但底层索引扫描不是。
在并行索引扫描或并行仅索引的扫描中, 协作进程轮流从索引读取数据。目前, 并行索引扫描仅支持btree索引。每个进程将声明一个索引块, 并将扫描并返回该块引用的所有元组;其他进程可以同时从不同的索引块返回元组。 并行btree扫描的结果以每个工作进程内的排序顺序返回。
其他扫描类型(如非Btree索引的扫描)可能会在将来支持并行扫描。
就像在非平行计划中一样,可以使用嵌套循环、 散列连接或合并连接将驱动表连接到一个或多个其他表。 连接的内侧可以是规划器支持的任何类型的非平行计划, 只要在并行工作者内运行是安全的。例如,如果选择了嵌套循环连接, 则内部计划可能是索引扫描,它会查找从连接外侧获取的值。
每个工作者将全部执行连接的内侧。这对于嵌套循环通常不是问题, 但对于涉及散列或合并连接的情况可能效率低下。例如,对于散列连接, 此限制意味着在每个工作进程中构建了一个相同的散列表, 这对于小型表中的连接工作良好,但在内部表很大时可能效率不高。 对于合并连接,这可能意味着每个工作者执行内部关系的一个单独排序, 这可能会很慢。当然,在这种并行计划效率低下的情况下, 查询规划器通常会选择其他计划(可能不使用并行性)。
PostgreSQL通过两个阶段的聚合来支持并行聚合。第一次,
每个参与查询计划并行部分执行的进程执行一个聚合步骤,
为进程发现的每个分组产生一个部分结果。这在计划中反映为一个
PartialAggregate
节点。第二次,部分结果被通过Gather
或Gather Merge
节点传输给领导者。最后,
领导者对所有工作者的部分结果进行重聚合以得到最终的结果。
这在计划中反映为一个FinalizeAggregate
节点。
因为Finalize Aggregate
节点运行在领导者进程上,
所以与输入行的数量相比产生相对大量组的查询对于查询计划者来说显得较不利。
例如,在最坏的情况下,Finalize Aggregate
节点看到的组的数量可能与Partial Aggregate
阶段中所有工作进程看到的输入行的数量一样多。对于这种情况,
使用并行聚合显然没有性能优势。查询规划器在规划过程中考虑到了这一点,
并且在这种情况下不太可能选择并行聚合。
并行聚合并不能支持所有的情况。每个聚合对于并行机制一定要是安全的,并且必须有一个结合函数。如果聚合有一个internal
类型的转移状态,它必须有序列化和反序列化函数。详见CREATE AGGREGATE。
如果任何聚合函数调用包含DISTINCT
或ORDER BY
子句,
则不支持并行聚合,并且对于有序集聚合或者查询涉及GROUPING SETS
时也不支持并行聚合。只有当查询中涉及的所有连接也是计划中并行不分的一部分时,才能使用并行聚合。
如果我们想要一个查询能产生并行计划但事实上又没有产生,可以尝试减小parallel_setup_cost或者parallel_tuple_cost。当然,这个计划可能比规划器优先产生的顺序计划还要慢,但也不总是如此。如果将这些设置为很小的值(例如把它们设置为零)也不能得到并行计划,那就可能是有某种原因导致查询规划器无法为你的查询产生并行计划。可能的原因可见第 15.2 节和第 15.4 节。
在执行一个并行计划时,可以用EXPLAIN (ANALYZE,VERBOSE)
来显示每个计划节点在每个工作者上的统计信息。这些信息有助于确定是否所有的工作被均匀地分发到所有计划节点以及从总体上理解计划的性能特点。