并行查询如何工作
并行查询(parallel query)是把一条查询中可以拆分的工作交给多个 CPU 核心同时执行,再由一个进程汇总结果。它最适合需要扫描、连接或聚合大量数据的查询;对于只读取少量行的查询,启动进程和合并结果的额外成本反而可能更高。
本文以 PostgreSQL 18 的执行模型为主。Gather、Parallel Seq Scan 等节点名称和配置项属于 PostgreSQL;其他数据库也可能支持并行执行,但进程模型、计划节点和限制并不相同。
先建立整体模型
普通串行查询只有一个后端进程,它依次执行整个计划。并行查询仍有一个负责客户端连接的协调进程(leader),但它可以申请若干并行工作进程(parallel worker),让它们共同执行计划中的并行部分。
一次典型的执行过程如下:
- 规划器估算不同执行方案的成本,判断并行方案是否比串行方案更便宜。
- 执行开始时,协调进程申请计划要求的工作进程;实际得到的数量可能更少,甚至为零。
- 协调进程和已启动的工作进程共同执行并行部分,例如分配表数据块、建立共享哈希表或计算局部聚合结果。
- 工作进程通过共享内存中的队列把结果送给协调进程。
Gather或Gather Merge汇集结果,协调进程继续执行上方的串行节点并把最终结果返回客户端。
协调进程不只是调度者。默认情况下,它也可以参与执行并行部分;但如果工作进程产生了大量结果,协调进程可能主要忙于读取和转发这些结果,能分担的计算就会减少。
计划中的 Workers Planned 只计算工作进程,不包含协调进程。例如,计划启动 2 个工作进程且协调进程也参与时,并行区最多有 3 个执行者。
:::info 并行执行不会改变事务语义
工作进程不是各自独立提交的事务。它们使用主查询传递的事务状态和数据快照,整条语句仍作为一个整体成功或失败。并行主要改变的是执行方式,不应改变 SQL 的结果集合。
:::
计划由串行区和并行区组成
在 PostgreSQL 计划中,Gather 或 Gather Merge 是并行区的边界:
- 它下面的计划由协调进程和工作进程执行;
- 它自己负责接收工作进程输出;
- 它上面的节点只由协调进程执行。
并行区中的节点不一定都能自动“分工”。只有具备 parallel-aware 能力的节点才会协调各进程,让它们处理互不重复的数据。普通节点如果出现在并行区,每个进程可能都要各自执行一遍,因此规划器只会在语义正确且成本合适时这样安排。
Gather:只收集,不保证顺序
Gather 从多个参与者接收行,按到达顺序向上游传递。不同进程的执行速度每次都可能不同,因此它不保留并行区输出的顺序。
Gather
Workers Planned: 2
-> Parallel Seq Scan on orders
如果查询没有顶层 ORDER BY,就不能依赖某次并行执行碰巧得到的行顺序。
Gather Merge:按既定顺序合并
Gather Merge 要求每个参与者提供已经按相同键排序的结果,然后像合并多条有序队列一样输出全局有序的数据:
Gather Merge
Workers Planned: 2
-> Sort
Sort Key: created_at
-> Parallel Seq Scan on orders
它能保留所需顺序,但每个参与者必须先产生有序输入,协调进程还要执行多路合并。若不需要顺序,普通 Gather 通常更简单。
一份并行聚合计划怎样阅读
假设要统计最近一年每个地区的订单金额:
EXPLAIN (ANALYZE, BUFFERS)
SELECT region, SUM(amount) AS total_amount
FROM orders
WHERE created_at >= DATE '2025-01-01'
GROUP BY region
ORDER BY region;
在数据量、索引、统计信息和配置合适时,计划可能近似如下:
Finalize GroupAggregate
Group Key: region
-> Gather Merge
Workers Planned: 2
Workers Launched: 2
-> Sort
Sort Key: region
-> Partial HashAggregate
Group Key: region
-> Parallel Seq Scan on orders
Filter: (created_at >= '2025-01-01'::date)
计划仍然要从缩进最深处向上读:
Parallel Seq Scan让参与者分担orders的数据块,并各自过滤日期。Partial HashAggregate在每个参与者内部先按地区计算局部状态。对于SUM,局部状态可以理解为各自的部分和。Sort把每个参与者的局部结果按region排序。Gather Merge合并多路有序结果。Finalize GroupAggregate由协调进程把同一地区的局部状态合成为最终金额。
这里的 Partial 和 Finalize 很重要。若所有原始行都直接传给协调进程再聚合,进程间通信和协调进程的压力会很大;先局部聚合可以显著减少需要传递的行数。
示例计划只是帮助理解结构。实际节点、工作进程数和成本会随 PostgreSQL 版本、数据分布、统计信息、索引及服务器配置变化。
常见并行执行节点
并行顺序扫描
Parallel Seq Scan 适合读取表中较大比例的数据。参与者通过共享状态认领数据块,通常不会重复扫描同一个块;每个参与者再检查自己所读块中的行是否满足条件。
并行顺序扫描并不是把表预先平均切成固定的几份。参与者按执行进度不断认领后续工作,因此某个进程变慢时,其他进程仍可继续领取数据块。这种动态分配有助于平衡扫描工作,但数据过滤、CPU 计算和 I/O 仍可能形成新的瓶颈。
并行位图堆扫描
Parallel Bitmap Heap Scan 会先由一个进程扫描一个或多个索引,生成需要访问哪些表数据块的位图,再让参与者分担这些数据块。它适合匹配行较多、普通索引扫描会产生大量随机读取,但全表扫描又读取过多无关数据的场景。
计划中常会同时看到 Bitmap Index Scan。建立位图与读取堆表的分工并不完全相同,判断并行效果时应关注整个子树,而不是只看节点名称中是否含有 Parallel。
并行索引扫描
PostgreSQL 的 B-tree 索引可用于 Parallel Index Scan 和 Parallel Index Only Scan。参与者协作领取索引中的数据页;每个参与者内部能按索引顺序读取,但普通 Gather 合并后不保证全局顺序。需要保序时,规划器可能选择 Gather Merge。
索引扫描是否值得并行,仍取决于需要访问的数据量。定位一两行的高选择性查询通常没有必要启动多个工作进程。
并行连接
连接是否受益,取决于连接算法以及两侧输入如何分工:
| 连接方式 | 并行执行时的典型行为 | 主要风险 |
|---|---|---|
| 嵌套循环连接 | 外侧输入可并行;每个参与者对自己取得的外侧行执行内侧查找 | 内侧缺少索引时,重复扫描的代价会被放大 |
| 合并连接 | 外侧输入可并行;各参与者可能需要独立处理内侧有序输入 | 重复排序或重复读取内侧数据可能抵消收益 |
| 普通哈希连接 | 外侧输入可并行,但每个参与者可能建立自己的内侧哈希表 | 哈希表占用和构建工作可能重复 |
| 并行哈希连接 | 参与者共同建立并探测一份共享哈希表 | 数据倾斜、内存不足或分批写盘会限制效果 |
因此,“连接下方出现并行扫描”不等于整个连接已经高效并行。还要检查内侧节点的 loops、哈希表内存与批次数,以及每个参与者处理的行数是否均衡。
并行聚合
能拆分的聚合通常采用两阶段执行:
Partial Aggregate或Partial HashAggregate:每个参与者生成局部聚合状态;Finalize Aggregate或Finalize GroupAggregate:协调进程合并状态并得到最终结果。
并非所有聚合函数都支持这种拆分。只要有聚合函数缺少合并局部状态所需的能力,或者聚合调用包含 DISTINCT、聚合内部的 ORDER BY,又或者查询使用有序集聚合、GROUPING SETS,PostgreSQL 就不能使用并行聚合。即使可以拆分,如果分组数量非常大,局部结果接近原始行数,汇总成本也可能超过收益。
并行追加
分区表或 UNION ALL 可能出现 Parallel Append。它让参与者动态领取不同子计划,而不要求每个参与者都执行全部分区。各分区大小差异较大时,动态领取通常比静态分配更容易保持负载均衡。
规划器何时会选择并行计划
规划器会比较串行与并行方案的估算成本。并行方案不仅要计算原本的扫描、连接和聚合成本,还要承担:
- 启动和管理工作进程的成本;
- 把行从工作进程传给协调进程的通信成本;
Gather之后只能由协调进程执行的串行工作;- 多进程争用 CPU、内存带宽、缓存和存储 I/O 的影响。
因此,下列查询通常更可能受益:
- 扫描大型表或大型索引中的大量数据;
- 对大量行执行 CPU 密集的表达式、连接或聚合;
- 过滤后能在工作进程内显著减少输出;
- 服务器有空闲 CPU,并且存储能提供足够吞吐量。
下列查询通常不适合并行:
- 通过索引快速定位极少量行;
- 执行时间本来就很短;
LIMIT能让串行索引扫描很快停止;- 并行区会产生大量行,协调进程成为汇集瓶颈;
- 服务器已经 CPU 饱和,或查询主要受单块慢磁盘限制;
- 数据严重倾斜,绝大多数工作落在少数参与者上。
并行加速不会随进程数线性增长。只由协调进程执行的部分、进程通信和共享资源争用共同决定了加速上限。工作进程从 2 个增加到 4 个,执行时间并不一定减半。
为什么没有出现并行计划
除了成本不划算,查询还必须满足并行安全要求。常见阻碍包括:
- 查询修改数据,或使用
SELECT ... FOR UPDATE、FOR SHARE等行锁定子句; - 调用了被标记为
PARALLEL UNSAFE的函数;用户自定义函数默认不会被当作并行安全函数; - 查询可能在执行中暂停,例如通过某些游标方式读取结果;
- 当前事务使用
SERIALIZABLE隔离级别; - 查询已在另一个并行工作进程内部执行,不能继续嵌套启动新的并行查询;
- 与并行工作进程的启动上限、会话配置或表大小阈值不符。
PostgreSQL 把函数和操作分为三类:
| 安全级别 | 含义 |
|---|---|
PARALLEL SAFE | 可以在并行工作进程中执行 |
PARALLEL RESTRICTED | 只能由协调进程执行,相关操作不能放到并行区内部 |
PARALLEL UNSAFE | 整条查询不能使用并行计划 |
给自定义函数错误地标记 PARALLEL SAFE 可能导致错误结果或运行时错误。只有确认函数不写数据库、不访问进程局部状态,并且所调用的所有能力都适合工作进程时,才能修改其并行安全属性。
:::note 数据修改不等于底层查询永远不能并行
PostgreSQL 对直接修改表的计划有严格限制,但 CREATE TABLE AS、SELECT INTO、CREATE MATERIALIZED VIEW 等命令的底层查询在满足条件时可以使用并行计划。应以目标 PostgreSQL 版本的实际 EXPLAIN 结果为准。
:::
影响并行计划的主要配置
下面是常见配置项。它们影响规划或资源上限,但修改其中一个参数并不保证产生更快的并行计划。
| 配置项 | 作用 |
|---|---|
max_parallel_workers_per_gather | 单个 Gather 或 Gather Merge 最多计划多少个工作进程;设为 0 会禁止并行查询计划 |
max_parallel_workers | 整个 PostgreSQL 实例可供并行操作使用的工作进程总上限 |
max_worker_processes | 实例中后台工作进程的总上限,也是更外层的资源约束 |
min_parallel_table_scan_size | 考虑并行表扫描所需的最小估算扫描数据量 |
min_parallel_index_scan_size | 考虑并行索引扫描所需的最小估算扫描数据量 |
parallel_setup_cost | 规划器对启动并行执行附加的成本估算 |
parallel_tuple_cost | 规划器对每行跨进程传输附加的成本估算 |
parallel_leader_participation | 是否允许协调进程参与 Gather 下方的并行计划 |
工作进程数同时受多个全局上限和当时系统负载约束。计划显示 Workers Planned: 4,执行时也可能只有 Workers Launched: 2。PostgreSQL 不会因此让查询失败;现有参与者会完成剩余工作,但运行时间可能比规划器预期更长。
每个工作进程还会消耗 CPU、内存和其他资源。某些节点的内存额度可能由多个参与者分别使用,所以不能只用单个查询的 work_mem 值估算总内存。提高并行度前应同时评估并发查询数量,而不是只优化一条查询的最快成绩。
用 EXPLAIN ANALYZE 判断是否有效
先用不执行查询的 EXPLAIN 检查计划形状,再在安全环境中使用:
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT ...;
重点观察:
- 计划是否真的有并行区。 查找
Gather、Gather Merge和其下方的并行节点。 - 计划与实际启动数量是否一致。 比较
Workers Planned和Workers Launched。 - 工作是否均衡。
VERBOSE的每工作进程统计可帮助发现某个参与者处理了远多于其他参与者的行。 - 数据是否尽早缩减。 过滤和局部聚合越靠近扫描端,通常越能减少进程间传输。
- 串行部分是否主导时间。
Gather上方的大排序、最终聚合或复杂函数仍然只由协调进程执行。 - 是否受 I/O 或临时文件限制。 结合
BUFFERS、排序方法、哈希批次数和临时块读写判断;CPU 并行无法消除慢存储瓶颈。 - 总吞吐量是否改善。 单条查询变快可能占用更多服务器资源,导致其他查询变慢,因此还要在接近真实并发的负载下测试。
EXPLAIN ANALYZE 会真正执行语句,并会增加计时开销。不要直接对来源不明的重查询或生产环境中的数据修改语句使用它。
常见误区
“工作进程越多越快”
进程越多,调度、通信和资源争用也越多。最佳并行度取决于串行比例、数据规模、硬件和同时运行的查询数量。
“看到 Gather 就说明整条查询都并行”
只有 Gather 下方是并行区,上方仍由协调进程执行。并行区内部也可能包含每个参与者重复执行的普通节点。
“没有并行计划就是参数太保守”
小查询可能本来就更适合串行执行。也可能存在并行不安全函数、行锁定、错误的行数估算或能够快速停止的索引路径。应先解释计划,再调整参数。
“并行查询可以保证固定顺序”
普通 Gather 按结果到达顺序收集行。只有 SQL 中明确写出 ORDER BY,结果顺序才有语义保证;即使计划使用 Gather Merge,应用也不应绕过 SQL 的排序规则作假设。
“单条查询变快就代表系统更快”
并行查询常用更多 CPU 和内存换取更短延迟。在报表窗口可能很划算,在高并发在线事务系统中却可能降低整体吞吐量。优化目标应同时包含单次延迟、资源峰值和系统吞吐量。
一套实用分析流程
- 用普通
EXPLAIN确认串行计划与并行计划的结构和估算差异。 - 检查统计信息、过滤条件和预计行数,避免在错误估算上讨论并行度。
- 在安全且接近生产数据规模的环境运行
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)。 - 比较计划与实际工作进程数、各进程行数、I/O、临时文件和总耗时。
- 分别测试典型参数和真实并发,不以一次缓存已预热的结果下结论。
- 只有证据表明并行启动成本或资源上限不合理时,才调整配置,并记录对其他查询的影响。
理解并行查询的关键,不是记住每个参数,而是抓住三件事:哪些工作被安全地拆分了、拆分后减少了多少总时间、额外消耗了多少系统资源。执行计划只是起点,最终结论必须来自真实数据规模和真实负载下的测量。