PostgreSQL SQL 语言:并行查询

本文档为PostgreSQL 9.6.0文档,本转载已得到原译者彭煜玮授权。

1. 并行查询如何工作

当优化器判断对于某一个特定的查询,并行查询是最快的执行策略时,优化器将创建一个查询计划。该计划包括一个 Gather 节点。下面是一个简单的例子:


EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%';
                                     QUERY PLAN                                      
-------------------------------------------------------------------------------------
 Gather  (cost=1000.00..217018.43 rows=1 width=97)
   Workers Planned: 2
   ->  Parallel Seq Scan on pgbench_accounts  (cost=0.00..216018.33 rows=1 width=97)
         Filter: (filler ~~ '%x%'::text)
(4 rows)

在所有的情形下,Gather节点都只有一个子计划,它是将被并行执行的计划的一部分。如果 Gather节点 位于计划树的最顶层,那么整个查询将并行执行。如果它位于计划树的其他位置,那么只有查询的那一部分会并行执行。在上面的例子中,查询只访问了一个表,因此除Gather节点本身之外只有一个计划节点。因为该计划节点是 Gather节点的孩子节点,所以它会并行执行。

使用 EXPLAIN命令, 你能看到规划器选择的工作者数量。当查询执行期间到达Gather节点时,实现用户会话的进程将会请求和规划器选中的工作者数量一样多的后台工作者进程 。任何时候能够存在的后台工作者进程的总数由max_worker_processes限制,因此一个并行查询可能会使用比规划中少的工作者来运行,甚至有可能根本不使用工作者。最优的计划可能取决于可用的工作者的数量,因此这可能会导致不好的查询性能。如果这种情况经常发生,那么就应当考虑一下提高max_worker_processes的值,这样更多的工作者可以同时运行;或者降低max_parallel_workers_per_gather,这样规划器会要求少一些的工作者。

为一个给定并行查询成功启动的后台工作者进程都将会执行Gather 节点的后代计划的一部分。这些工作者的领导者也将执行该计划,不过它还有一个额外的任务:它还必须读取所有由工作者产生的元组。当整个计划的并行部分只产生了少量元组时,领导者通常将表现为一个额外的加速查询执行的工作者。反过来,当计划的并行部分产生大量的元组时,领导者将几乎全用来读取由工作者产生的元组并且执行Gather节点上层计划节点所要求的任何进一步处理。在这些情况下,领导者所作的执行并行部分的工作将会很少。

2. 何时会用到并行查询?

有几种设置会导致查询规划器在任何情况下都不生成并行查询计划。为了让并行查询计划能够被生成,必须配置好下列设置。

  • max_parallel_workers_per_gather必须被设置为大于零的值。这是一种特殊情况,更加普遍的原则是所用的工作者数量不能超过max_parallel_workers_per_gather所配置的数量。
  • dynamic_shared_memory_type必须被设置为除none之外的值。并行查询要求动态共享内存以便在合作的进程之间传递数据。

此外,系统一定不能运行在单用户模式下。因为在单用户模式下,整个数据库系统运行在单个进程中,没有后台工作者进程可用。

如果下面的任一条件为真,即便对一个给定查询通常可以产生并行查询计划,规划器都不会为它产生并行查询计划:

  • 查询要写任何数据或者锁定任何数据库行。如果一个查询在顶层或者 CTE 中包含了数据修改操作,那么不会为该查询产生并行计划。这是当前实现的一个限制,未来的版本中可能会有所改进。
  • 查询可能在执行过程中被暂停。只要在系统认为可能发生部分或者增量式执行,就不会产生并行计划。例如:用DECLARE CURSOR创建的游标将永远不会使用并行计划。类似地,一个FOR x IN query LOOP .. END LOOP形式的 PL/pgsql 循环也永远 不会使用并行计划,因为当并行查询进行时,并行查询系统无法验证循环中的代码执行起来是安全的。
  • 使用了任何被标记为PARALLEL UNSAFE的函数的查询。大多数系统定义的函数都被标记为PARALLEL SAFE,但是用户定义的函数默认被标记为PARALLEL UNSAFE。参见Section 15.4中的讨论。
  • 该查询运行在另一个已经存在的并行查询内部。例如,如果一个被并行查询调用的函数自己发出一个 SQL 查询,那么该查询将不会使用并行计划。这是当前实现的一个限制,但是或许不值得移除这个限制,因为它会导致单个查询使用大量的进程。
  • 事务隔离级别是可串行化。这是当前实现的一个限制。

即使对于一个特定的查询已经产生了并行查询计划,在一些情况下执行时也不会并行执行该计划。如果发生这种情况,那么领导者将会自己执行该计划在Gather节点之下的部分,就好像Gather节点不存在一样。上述情况将在满足下面的任一条件时发生:

  • 因为后台工作者进程的总数不能超过max_worker_processes,导致不能得到后台工作者进程。
  • 客户端发送了一个执行消息,并且消息中要求取元组的数量不为零。执行消息可见扩展查询协议中的讨论。因为libpq当前没有提供方法来发送这种消息,所以这种情况只可能发生在不依赖 libpq 的客户端中。如果这种情况经常发生,那在它可能发生的会话中设置 max_parallel_workers_per_gather是一个很好的主意,这样可以避免产生连续运行时次优的查询计划。
  • 事务隔离级别是可串行化。这种情况通常不会出现,因为当事务隔离级别是可串行化时不会产生并行查询计划。不过,如果在产生计划之后并且在执行计划之前把事务隔离级别改成可串行化,这种情况就有可能发生。

3. 并行计划

因为每个工作者只执行完成计划的并行部分,所以不可能简单地产生一个普通查询计划并使用多个工作者运行它。每个工作者都会产生输出结果集的一个完全拷贝,因而查询并不会比普通查询运行得更快甚至还会产生不正确的结果。相反,计划的并行部分一定被查询优化器在内部当作一个部分计划。也就是说,一定要这样来创建计划,使得每个将执行该计划的进程只产生输出行的一个子集,这样可以保证每个需要被输出的行刚好会被合作进程产生一次。

3.1. 并行扫描

当前唯一一种被修改用于并行查询的扫描类型是顺序扫描。因此在并行计划中的驱动表将总是被使用并行顺序扫描进行扫描。关系的块将被划分给合作进程。一次发放一个文件块,这样对于关系的访问仍然保持为顺序访问。在请求一个新页面之前,每一个进程将访问分配给它的页面上的每一个元组。

3.2. 并行连接

驱动表将被使用嵌套循环或者哈希连接连接到一个或者多个其他表。在连接的外侧可以是任何一种被规划器支持可以安全地在并行工作者中运行的非并行计划。例如,它可以是一个索引扫描,基于从内表取得的一列来查找值。每个工作者都将会完整地执行外侧的计划,这也是为什么这里不能支持归并连接的原因。归并连接的外侧常常涉及到排序整个内表,即便使用索引,多次在内表上进行完全索引扫描也效率不高。

3.3. 并行聚集

将查询的聚集部分整个地并行执行是不可能的。例如,如果一个查询涉及到选择COUNT(*),每个工作者可以计算一个总和,但是这些总和需要被整合在一起以产生最终的答案。如果一个计划涉及到GROUP BY子句,为每个组需要计算出一个单独的总和。即使聚集不能完全地并行执行,但是涉及聚集的查询常常是并行查询很好的候选,因为它们通常都是读很多行但只返回少数几行给客户端。返回很多行给客户端的查询常常受制于客户端读取数据的速度,这种情况下并行查询帮助不大。

PostgreSQL通过做两次聚集来支持并行聚集。第一次,每个参与查询计划并行部分执行的进程执行一个聚集步骤,为进程发现的每个分组产生一个部分结果。这在计划中反映为一个PartialAggregate节点。第二次,部分结果被通过Gather节点传输给领导者。最后,领导者对所有工作者的部分结果进行重聚集以得到最终的结果。这在计划中反映为一个FinalizeAggregate节点。

并行聚集并不能支持所有的情况。每个聚集对于并行机制一定要是安全的,并且必须有一个结合函数。如果聚集有一个internal类型的转移状态,它必须有序列化和反序列化函数。对于有序集聚集或者查询涉及GROUPING SETS时不支持并行聚集。只有当查询中涉及的所有连接也是计划中并行不分的一部分时,才能使用并行聚集。

3.4. 并行计划小贴士

如果我们想要一个查询能产生并行计划但事实上又没有产生,可以尝试减小parallel_setup_cost或者parallel_tuple_cost。当然,这个计划可能比规划器优先产生的顺序计划还要慢,但也不总是如此。如果将这些设置为很小的值(例如把它们设置为零)也不能得到并行计划,那就可能是有某种原因导致查询规划器无法为你的查询产生并行计划。

在执行一个并行计划时,可以用EXPLAIN (ANALYZE,VERBOSE)来显示每个计划节点在每个工作者上的统计信息。这些信息有助于确定是否所有的工作被均匀地分发到所有计划节点以及从总体上理解计划的性能特点。

4. 并行安全性

规划器把查询中涉及的操作分类成并行安全、并行受限或者并行不安全。并行安全的操作不会与并行查询的使用产生冲突。并行受限的操作不能在并行工作者中执行,但是能够在并行查询的领导者中执行。因此,并行受限的操作不能出现在Gather节点之下,但是能够出现在包含有Gather 节点的计划的其他位置。并行不安全的操作不能在并行查询中执行,甚至不能在领导者中执行。当一个查询包含任何并行不安全操作时,并行查询对这个查询是完全被禁用的。

下面的操作总是并行受限的。

  • 公共表表达式(CTE)的扫描。
  • 临时表的扫描。
  • 外部表的扫描,除非外部数据包装器有一个IsForeignScanParallelSafe API。
  • 对InitPlan或者SubPlan的访问。

4.1. 为函数和聚集加并行标签

规划器无法自动判定一个用户定义的函数或者聚集是并行安全、并行受限还是并行不安全,因为这需要预测函数可能执行的每一个操作。一般而言,这就相当于一个停机问题,因此是不可能的。甚至对于可以做到判定的简单函数我们也不会尝试,因为那会非常昂贵而且容易出错。相反,除非是被标记出来,所有用户定义的函数都被认为是并行不安全的。在使用CREATE FUNCTION或者ALTER FUNCTION时,可以通过指定PARALLEL SAFE、PARALLEL RESTRICTED或者PARALLEL UNSAFE来设置标记 。在使用CREATE AGGREGATE时,PARALLEL选项可以被指定为SAFE、RESTRICTED或者 UNSAFE。

如果函数和聚集会写数据库、访问序列、改变事务状态(即便是临时改变,例如建立一个EXCEPTION块来捕捉错误的 PL/pgsql)或者对设置做持久化的更改,它们一定要被标记为PARALLEL UNSAFE。类似地,如果函数会访问临时表、客户端连接状态、游标、预备语句或者系统无法在工作者之间同步的后端本地状态,它们必须被标记为PARALLEL RESTRICTED。例如,setseed和 random由于后一种原因而是并行受限的。

一般而言,如果一个函数是受限或者不安全的却被标记为安全,或者它实际是不安全的却被标记为受限,把它用在并行查询中时可能会抛出错误或者产生错误的回答。如果 C 语言函数被错误标记,理论上它会展现出完全不明确的行为,因为系统中无法保护自身不受任意 C 代码的影响。但是,在最有可能的情况下,结果不会比其他任何函数更糟糕。如果有疑虑,最好还是标记函数为UNSAFE。

如果在并行工作者中执行的函数要求领导者没有持有的锁,例如读该查询中没有引用的表,那么工作者退出时会释放那些锁(而不是在事务结束时释放)。如果你写了一个这样做的函数并且这种不同的行为对你很重要,把这类函数标记为PARALLEL RESTRICTED以确保它们只在领导者中执行。

注意查询规划器不会为了获取一个更好的计划而考虑延迟计算并行受限的函数或者聚集。所以,如果一个被应用到特定表的WHERE子句是并行受限的,查询规划器就不会考虑把对那张表的扫描放置在Gather节点之下。在一些情况中,可以(甚至效率更高)把对表的扫描包括在查询的并行部分并且延迟对WHERE子句的计算,这样它会出现在Gather节点之上。不过,规划器不会这样做。

上一篇:QMenu 下的 QAction 文字按对齐方式显示


下一篇:java中包容易出现的错误及权限问题