1.Base基础/3.Icon图标/操作/search备份
1.Base基础/3.Icon图标/操作/search备份
EN
文档
关于AntDB
部署与升级
快速入门
运维
调优
工具和插件
高级服务
数据安全
参考
  • 文档首页 /
  • 使用教程 /
  • SQL语言 /
  • 并行查询

并行查询

更新时间:2024-07-01 14:39:49

概述

AntDB 能设计出利用多 CPU 让查询更快的查询计划,这种特性被称为并行查询。由于现有实现的限制或者因为没有比连续查询计划更快的查询计划存在,很多查询并不能从并行查询获益。不过,对于那些可以从并行查询获益的查询来说,并行查询带来的速度提升是显著的。很多查询在使用并行查询时比之前快了超过两倍,有些查询是以前的四倍甚至更多的倍数。那些访问大量数据但只返回其中少数行给用户的查询最能从并行查询中获益。这一章介绍一些并行查询如何工作的细节以及哪些情况下可以使用并行查询,这样希望充分利用并行查询的用户可以理解他们能从并行查询得到什么。

并行查询如何工作

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

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)

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

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

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

当计划的并行部分的顶层节点是 Gather Merge 而不是 Gather 时,它表示每个执行计划并行部分的进程会产生有序的元组,并且领导者执行一种保持顺序的合并。相反,Gather 会以任何方便的顺序从工作者读取元组,这会破坏可能已经存在的排序顺序。

何时会用到并行查询?

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

  • max_parallel_workers_per_gather 必须被设置为大于零的值。这是一种特殊情况,更加普遍的原则是所用的工作者数量不能超过 max_parallel_workers_per_gather 所配置的数量。

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

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

  • 查询要写任何数据或者锁定任何数据库行。如果一个查询在顶层或者 CTE 中包含了数据修改操作,那么不会为该查询产生并行计划。一种例外是,CREATE TABLE ... ASSELECT INTO 以及 CREATE MATERIALIZED VIEW 这些创建新表并填充它的命令可以使用并行计划。
  • 查询可能在执行过程中被暂停。只要在系统认为可能发生部分或者增量式执行,就不会产生并行计划。例如:用 DECLARE CURSOR 创建的游标将永远不会使用并行计划。类似地,一个 FOR x IN query LOOP .. END LOOP 形式的 PL/pgSQL 循环也永远不会使用并行计划,因为当并行查询进行时,并行查询系统无法验证循环中的代码执行起来是安全的。
  • 使用了任何被标记为 PARALLEL UNSAFE 的函数的查询。大多数系统定义的函数都被标记为 PARALLEL SAFE,但是用户定义的函数默认被标记为 PARALLEL UNSAFE
  • 该查询运行在另一个已经存在的并行查询内部。例如,如果一个被并行查询调用的函数自己发出一个 SQL 查询,那么该查询将不会使用并行计划。这是当前实现的一个限制,但是或许不值得移除这个限制,因为它会导致单个查询使用大量的进程。

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

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

并行计划

因为每个工作者只执行完成计划的并行部分,所以不可能简单地产生一个普通查询计划并使用多个工作者运行它。每个工作者都会产生输出结果集的一个完全拷贝,因而查询并不会比普通查询运行得更快甚至还会产生不正确的结果。相反,计划的并行部分一定被查询优化器在内部当作一个部分计划,即它必须被构建出来,这样每一个执行该计划的进程将以无重复地方式产生输出行的一个子集,即保证每一个所需要的输出行正好只被一个合作进程生成。通常,这意味着该查询的驱动表上的扫描必须是一种可并行的扫描。

并行扫描

当前支持下列可并行的表扫描。

  • 在一个并行顺序扫描中,表块将在合作进程之间被划分。一次会分发一个块,这样对表的访问还是保持顺序方式。
  • 在一个并行位图堆扫描中,一个进程被选为领导者。这个进程执行对一个或者多个索引的扫描并且构建出一个位图指示需要访问哪些表块。这些表块接着会在合作进程之间划分(和并行顺序扫描中一样)。换句话说,堆扫描以并行方式进行但底层的索引扫描不是并行。
  • 在一个并行索引扫描或者并行只用索引的扫描中,合作进程轮流从索引读取数据。当前,并行索引扫描仅有 B- 树索引支持。每一个进程将认领一个索引块并且扫描和返回该索引块引用的所有元组,其他进程可以同时地从一个不同的索引块返回元组。并行 B- 树扫描的结果会以每个工作者进程内的顺序返回。

其他扫描类型(例如非 B- 树索引的扫描)可能会在未来支持并行扫描。

并行连接

正如在非并行计划中那样,驱动表可能被使用嵌套循环、哈希连接或者归并连接连接到一个或者多个其他表。连接的内侧可以是任何类型的被规划器支持的非并行计划,假设它能够安全地在并行工作者中运行。根据连接类型,内侧还可以是一种并行计划。

  • 在一个嵌套循环连接中,内侧总是非并行的。尽管它会被完全执行,如果内侧是一个索引扫描也会很高效,因为外侧元组以及在索引中查找值的循环会被划分到多个合作进程。
  • 在一个归并连接中,内侧总是一个非并行计划并且因此会被完全执行。这可能是不太高效的,特别是在排序必须被执行时,因为在每一个合作进程中工作数据和结果数据是重复的。
  • 在一个哈希连接(没有“并行”前缀)中,每个合作进程都会完全执行内侧以构建哈希表的相同拷贝。如果哈希表很大或者该计划开销很大,这种方式就很低效。在一个并行哈希连接中,内侧是一个并行哈希,它把构建共享哈希表的工作划分到多个合作进程。

并行聚集

AntDB 通过按两个阶段进行聚集来支持并行聚集。首先,每个参与到查询并行部分的进程执行一个聚集步骤,为该进程注意到的每个分组产生一个部分结果。这在计划中反映为一个 Partial Aggregate 节点。然后,部分结果通过 Gather 或者 Gather Merge 被传输到领导者。最后,领导者对来自所有工作者的结果进行重新聚集得到最终的结果。这在计划中反映为一个 Finalize Aggregate 节点。

因为 Finalize Aggregate 节点运行在领导者进程上,如果查询产生的分组数相对于其输入行数来说比较大,则查询规划器不会喜欢它。例如,在最坏的情况下,Finalize Aggregate 节点看到的分组数可能与所有工作者进程在 Partial Aggregate 阶段看到的输入行数一样多。对于这类情况,使用并行聚集显然得不到性能收益。查询规划器会在规划过程中考虑这一点并且不太会在这种情况下选择并行聚集。

并行聚集并非在所有情况下都被支持。每一个聚集都必须是对并行安全的并且必须有一个组合函数。如果该聚集有一个类型为 internal 的转移状态,它必须有序列化和反序列化函数。更多细节请参考 CREATE AGGREGATE。如果任何聚集函数调用包含 DISTINCTORDER BY 子句,则不支持并行聚集。对于有序集聚集或者当查询涉及 GROUPING SETS 时,也不支持并行聚集。只有在查询中涉及的所有连接也是该计划并行部分的组成部分时,才能使用并行聚集。

并行 Append

只要当 AntDB 需要从多个源中整合行到一个单一结果集时,它会使用 AppendMergeAppend 计划节点。在实现 UNION ALL 或扫描分区表时常常会发生这种情况。就像这些节点可以被用在任何其他计划中一样,它们可以被用在并行计划中。不过,在并行计划中,规划器使用的是 Parallel Append 节点。

当一个 Append 节点被用在并行计划中时,每个进程将按照子计划出现的顺序执行子计划,这样所有的参与进程会合作执行第一个子计划直到它被完成,然后同时移动到第二个计划。而在使用 Parallel Append 时,执行器将把它的子计划尽可能均匀地散布在参与进程中,这样多个子计划会被同时执行。这避免了竞争,也避免了子计划在那些不执行它的进程中产生启动代价。

此外,和常规的 Append 节点不同(在并行计划中使用时仅有部分子计划),Parallel Append 节点既可以有部分子计划也可以有非部分子计划。非部分子计划将仅被单个进程扫描,因为扫描它们不止一次会产生重复的结果。因此涉及到追加多个结果集的计划即使在没有有效的部分计划可用时,也能实现粗粒度的并行。例如,考虑一个针对分区表的查询,它只能通过使用一个不支持并行扫描的索引来实现。规划器可能会选择常规 Index Scan 计划的 Parallel Append。每个索引扫描必须被单一的进程执行完,但不同的扫描可以由不同的进程同时执行。

enable_parallel_append 可以被用来禁用这种特性。

并行计划小贴士

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

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

并行安全性

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

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

  • 公共表表达式(CTE)的扫描。
  • 临时表的扫描。
  • 外部表的扫描,除非外部数据包装器有一个 IsForeignScanParallelSafe API。
  • InitPlan 所挂接到的计划节点。
  • 引用一个相关的 SubPlan 的计划节点。

为函数和聚集加并行标签

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

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

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

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

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

问题反馈