PDF版本的下载链接:PostgreSQL并行查询PDF
PostgreSQL可以制定哪些SQL可以并行利用CPU的查询规划,用于增快SQL查询的响应速度。这个特性以并行查询而为大众所知。有些查询不能够从并行查询之中获益,要么受限于当前的实现,要么由于并行查询并不比串行查询规划快。然而,对于那些可以从并行查询中收益的查询而言,并行查询加速的效果是非常明显的。有些查询可以在并行查询中快两倍,有些查询可以快四倍甚至更多。那些访问大量数据,却返回给用户很少行数的查询是并行查询效果收益最明显的(译者注:例如统计型查询)。本章介绍了并行查询如何工作的细节,以及它适用于哪些场景,因此用户可以了解到如何使用它。
当优化器意识对于一个特定的查询语句并行查询速度更快,它将会创建一个包括聚集节点的执行规划。这有一个简单示例:
在所有场景下,聚集节点只会有一个子规划,它是查询规划的一部分并且会被并行执行。如果聚集节点是在规划树的顶部,那么整个查询将会被并行执行。如果它在规划树的其他部分,那么查询就只有一部分会被并行执行。在上面示例的这个查询中,由于这个查询只访问了一张表,所以除了聚集节点外就只有一个规划节点,因为规划节点是聚集节点的子节点,所以它将会被并行执行。
使用explain,你将可以看到规划器选择了多少个worker。当在查询执行中访问到聚集节点时,将会申请和规划器选择的worker数目一样多的后台工作进程。在任何时刻后台可以存在的worker的个数是由max_worker_processes参数限制的,所以并行的运行一系列的查询同少量的worker甚至有的查询是没有worker在一起是可行的。优化策略取决于当前还有多少worker可用,所以它可能会导致比较糟糕的执行效果。如果并行查询是频繁的,那么提升max_worker_processes 的值让更多worker可以同时执行,或者有选择的降低max_parallel_workers_per_gather的值使得一个规划器能够获取的worker数目降低。
由一个给定的并行查询启动的每个后台工作进程将会执行查询的一部分(这些部分都是聚集节点的后继节点)。leader也会执行查询的一部分,但它有附加的任务:读取由worker产生的所有元组。如果规划的并行部分输出少量的元组,那么leader更像一个附加的工作节点,加快查询速度。与之相对的,如果规划的并行部分产生了大量的元组,那么leader将会更多的整个用于处理由工作节点产生的元组,以及执行由聚集节点的上层规划节点指定的处理任务。在此场景下,leader只会做规划中并行部分的少部分工作。
有一些配置可以使得查询规划器在任何场景下都不产生并行查询的规划。为了产生并行查询规划,下面的参数必须得配置:
max_parallel_workers_per_gather必须设置为一个大于0的值。这是一个通用的限制,就是worker的数目不能够大于max_parallel_workers_per_gather(译者注:对于每一个并行查询而言)。
dynamic_shared_memory_type必须设置为一个非none值。并行查询需要更多的共享内存,用于在合作的进程直接传递数据。
另外,系统不能运行在单用户模式下。如果整个系统以单进程的方式运行,那么就不会有后台worker可被使用。
甚至如果在可能产生并行查询规划时,如果出现了下述情况,规划器将不会产生并行查询规划:
l 如果一个查询写入任何数据或者锁了数据库的任何行。如果一个查询语句包含了数据修改操作,不论是在顶层还是在CTE层,该查询语句将不会生成并行规划。这是由于当前的实现所限制的,并且会将在未来的发布版本中被移除。
l 在执行中会被挂起的查询。在任何情况下如果系统认为部分或者增量执行会产生,那么将不会产生并行规划。例如,使用DECLARE CURSOR建立游标将不会产生并行规划。与之类似的,使用PL/PSql循环 FOR x IN query LOOP .. END LOOP 也不会产生并行规划,因为并行查询系统不能确定在循环里的代码在并行查询中运行是安全的。
l 任何使用了被标记为 PARALLELUNSAFE方法的查询。大多数系统定义的函数是PARALLEL SAFE的。但是用户自定义的函数是默认被标记为PARALLEL UNSAFE的。可以看15.4部分的讨论。
l 在一个已经并行运行的查询中运行的查询。例如,一个被并行查询调用的方法,这个查询将永远不会使用并行规划。这是受当前实现的限制,不过移除它有点不值得,因为这有可能导致一个简单的查询使用了大量的进程。
l 事务独立级别是串行的。这是受当前实现的限制。
甚至当一个查询产生了并行查询规划,但也有一些情况会导致在真正查询时并行是不可能的。当遇到这种情况时,leader节点将会执行聚集节点下的整个规划,这就类似于聚集节点不存在一样。当下列情况出现时,上述情况会发生:
l 由于max_worker_processes参数对于后台worker总数的限制,没有后台worker可以被使用了
l 客户端发送了非零取数的执行命令。详见extended queryprotocol中的讨论。因为libpg当前并没有任何方式支持这种命令,所以这个场景只会出现在非依赖libpg的客户端上。如果这个频繁发生,那么在会话中设置 max_parallel_workers_per_gather参数值是一个好办法,这样可以避免产生欠理想的执行规划串行执行。
l 一个预处理命令,使用CREATETABLE .. AS EXECUTE ..这样的语句。这使得一个只读操作变为读写操作,使得语句无权并行执行。
l 事务隔离是串行的。这个也许不经常发生,因为事务隔离时不会产生并行查询规划。但是,如果在执行规划产生后在执行开始前改变了事务隔离等级,那么这个就会发生。
因为每个worker执行规划中的一部分。这就肯定不会是简单的生成原来的那种执行规划,然后再交给每个worker去执行。如果这样弄的话,每个worker都会产生一份完整的结果,这样不仅不会使得查询速度变快,而且会导致产生不正确的结果。取而代之的是,并行规划作为执行规划的一部分被执行规划器内部知晓,这样每个进程都生成结果集的一部分子集,即输出结果的每一行都由合作进程中的一个特定的进程保障。
当前,只有顺序扫描可以被修改为并行查询。因此,一个表扫描进入并行查询,通常使用的是Parallel Seq Scan(并行顺序扫描)。关系表的块被分配给合作进程的每一个进程。块被一次分发出去,所以访问关系表依然是顺序的。每一个进程在请求一个新页之前,必须访问被分配页的每一个元组。
一个表也许使用嵌套循环或者hash join去关联一张或者多张表。Outer join也许不会产生并行规划,除非规划器证明它运行在多个worker上是安全的。例如,也许有对于内部表的一个列用某个值进行索引扫描。每个worker都需要执行规划的外部部分,这就是为什么merge join在这儿是不被支持的。(译者注:此处完全没有看懂)。在merge join的外部部分通常包括排序的内部表,所以这就是为什么不倾向于生成多个进程每个对内部表进行一个索引扫描。
对于一个聚合查询整个的并行是不可能的。例如,一个查询包括select count(*),每个worker需要计算一个总值,但是这些总值需要被相加以产生一个最终的结果。如果查询包含group by,那么需要为每个group进行独立的值统计。虽然聚合查询不能整个的并行,但是包含聚合的查询在使用并行查询时通常表现非常卓越,这是因为他们通常读取很多行但只返回给客户端少量行。返回给客户端许多行的查询速度,通常受限于客户端的读取速度,那么这种情况并行查询就爱莫能助了。
PostgreSQL通过聚合两次来支持并行聚合。首先,每一个进程执行聚合阶段的一部分,产生group by的中间结果。这个体现在PartialAggregate节点的执行规划上。然后,这些中间结果通过聚集节点传递给leader,由leader对于所有worker的结果进行再次聚合用于产生最终结果。这个体现在FinalizeAggregate节点的规划上。
并行聚合并不是在所有情况下都被支持。每个聚合必须是并行安全的,并且必须有一个聚合函数。如果聚合在内部存在过渡状态的话,那么它必须有序列化和反序列化方法。查阅CREATE AGGREGATE来了解更多细节。对于有序集的聚合或者使用GROUPING SET的聚合,并行聚合是不被支持的。并行聚合只能用在,在查询中包括所有的join并且也是并行规划的一部分的情况。
如果一个查询期望并行执行,却没有产生并行规划,那么你可以尝试调低 parallel_setup_cost和parallel_tuple_cost。当然,这个规划也许会体现得比规划器选择的串行执行更慢,但是它也许并非经常如此。如果,你设置了这些参数一个极小的值(例如,将它们都设为0)但仍旧没有得到并行规划,那么也许规划器为何没法产生并行规划就是有原因的了。从15.2或者15.4小节了解这些情况。
当执行一个并行查询,你可以使用EXPLAIN(ANALYZE,VERBOSE)来展示每一个规划节点每一个woker的统计信息。在确定工作是否均衡的分布在各个节点上,以及更好的了解规划的特性上,这是非常有用的。
规划器将操作分类未并行安全、并行受限以及非并行安全。一个并行安全的操作在使用并行查询时是不受限制的。一个并行受限的操作是不能够在并行worker上执行的,但是它可以被并行查询的leader执行。因此,一个并行受限的操作不会出现在低于聚集节点的节点上,但是会出现在包含聚集节点的规划上。一个非并行安全操作是不能够使用并行查询的,甚至在leader上。当一个查询包含非并行安全操作,那么对于整个查询并行查询是被禁止的。
下列操作是并行受限的:
l 扫描通用表描述(CTEs)
l 扫描临时表
l 扫描外部表,或者外部数据被IsForeignScanParallelSafe包裹
l 访问InitPlan或者SubPlan
规划器不会自动的区分用户定义的函数是并行安全的、并行受限的还是非并行安全的,因为这需要预测每一个函数的行为。通常,这是一个很复杂的问题并且是不可解决的。甚至对于简单的函数,我们也没有尝试,因为这样代价很大并且容易引入bug。取而代之的是,用户定义的函数除非自己定义了标签,否则会被默认标记为非并行安全。当使用CREATE FUNCTION或者ALTER FUNCTION可以标记 PARALLEL SAFE, PARALLEL RESTRICTED, 或者PARALLELUNSAFE。 当使用CREATE AGGREGATE可以标记SAFE, RESTRICTED, or UNSAFE。
函数或者聚合必须被标记为PARALLEL UNSAFE如果他们写库、访问序列、修改事务级别甚至是临时修改(例如,一个PL/PGSQL方法设置一个EXCEPTION块来捕获异常),或者对于设置进行持久化修改。与之类似的,函数必须被标记为PARALLEL RESTRICTED如果他们访问临时表,客户连接状态、游标、预处理语句、后台本地状态系统不能够同步worker。例如,setseed或者random是并行受限的。
通常,一个函数是并行受限或者非并行安全的被标记为安全,或者事实上是非并行安全的被标记为并行受限,在并行执行时它会抛出异常或者产生错误的结果。C语言级别的函数在理论上阻止了错误标记的函数运行时产生未定义的行为,因为除了二进制C语言代码之外系统是没有办法保护自身的,但是结果不会比其他方法产生的结果更糟。如果存疑的话,那么就最好将方法标记为非并行安全。
如果并行worker当中执行的方法申请了锁,而它不是leader所有的,例如请求一个不在查询中声明的表,这些锁将会在worker退出时释放,而不是事务结束时释放。如果你写了一个做了这个事情的方法,标记这些方法为并行受限的,这样它们就只会被leader执行。
注意,查询规划器并不评估一个并行受限的函数或者聚合来产生一个更高级别的规划。因此,例如where条件在一个并行受限的表上,那么规划器将不会将扫描这张表放到聚集节点之下。在一些场景下这是有可能的(或者更有效率的)将对于这张表的扫描放在并行部分并且对where条件进行评估,它的执行效果是优于放在聚集节点之上的。但是,规划器并不会这样做。