帮你快速理解、总结文档立即下载

并行查询计划解读

最近更新时间:2026-08-20 15:23:31
我的收藏

概述

本文介绍如何通过 EXPLAIN 系列命令查看 TDSQL Boundless 并行查询的执行计划,包括基本并行标识、树形格式节点含义、执行统计分析,以及如何用 Optimizer Trace 进行基本的并行计划排查。如果想要进一步解读并行计划,请参考 并行计划进阶解读

基本并行标识(传统 EXPLAIN 表格格式)

使用传统 EXPLAIN(不带 FORMAT=TREE)时,并行查询通过 Extra 列中的标识来识别。

Parallel scan (N workers) 标识

当某张表被选为并行扫描对象时,其 Extra 列会显示 Parallel scan (N workers),其中 N 为并行度(Degree of Parallelism, DOP):
tdsql> EXPLAIN SELECT COUNT(*) FROM t1 WHERE a > 0;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------------------------------------------+
| 1 | SIMPLE | t1 | NULL | ALL | NULL | NULL | NULL | NULL | 10000| 33.33 | Using where; Parallel scan (4 workers); Using pushed condition (`test`.`t1`.`a` > 0); Using projection pushdown |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------------------------------------------+
Extra 列中 Parallel scan (4 workers) 表示该查询使用了4个 Worker 并行执行,是一个并行扫描表。
说明:
当查询使用了并行计划但使用传统 EXPLAIN 查看时,系统会输出一条 Note (Code 1003): Query is executed in a parallel plan; explain with tree format to see the plan details.
对于并行查询,建议始终使用 EXPLAIN FORMAT=TREE 查看完整的计划详情。

EXPLAIN FORMAT=TREE 树形格式

树形格式以缩进方式展示执行计划,是查看并行查询计划的最佳方式。并行查询引入了以下专有节点和标注。

Gather 节点

Gather 是并行计划的核心节点,是一种 Collector,位于 Leader 侧,负责收集各 Worker 的并行执行结果。
-> Gather (slice: 1, workers: 4) (cost=2308.83..2308.83 rows=1)
-> Aggregate: count(0) (cost=4.33..4.33 rows=1)
-> Index range scan on p1 using k_a, with range parallel scan (cost=3.56..3.56 rows=1)
字段含义
字段
说明
slice: N
该 Gather 对应的发送端 Slice 编号
workers: N
参与并行执行的 Worker 数量(即 DOP)
Gather 节点下方的子树为 PartialPlan(Worker 侧执行的局部计划),每个 Worker 会独立执行该子树。

Merge Sort 节点

当并行查询需要全局排序时,各 Worker 先各自排序,然后由 Gather 节点执行归并排序(Merge Sort)。
普通归并排序
-> Gather (slice: 1, workers: 4) (cost=3475972.28..7673667.78 rows=10000000)
Merge sort: t1.a
-> Sort: t1.a (cost=3472492.60..3472492.60 rows=2500000)
-> Stream results (cost=1728.47..894445.59 rows=2500000)
-> Inner hash join (t2.b = t1.a)
-> Table scan on t2
-> Hash
-> ...
带去重的归并排序(用于 SELECT DISTINCTGROUP BY 场景):
-> Gather (slice: 1, workers: 4) (cost=41009223.24..41009223.24 rows=60000000)
Merge sort with duplicate removal: c1
-> Sort with duplicate removal: c1 (cost=41009223.24..41009223.24 rows=15000000)
-> Table scan on <temporary>
-> Aggregate using temporary table
-> Left hash join (t2.a = t1.col1)
-> Index scan on t1 using date_col, with parallel scan ranges: 2, distinct prefix: date_col
-> Hash
-> Table scan on t2

并行扫描标注

并行扫描信息附加在表扫描节点的描述中,有两种扫描模式:

Range Parallel Scan(动态范围并行扫描)

按数据范围分片,各 Worker 扫描不同的范围:
-> Table scan on t1, with range parallel scan (cost=0.29..737.33 rows=2500)
EXPLAIN ANALYZE 中还会显示实际范围数:
-> Table scan on t1, with range parallel scan (ranges: 12) (cost=0.29..737.33 rows=2500)
如果使用了 distinct prefix 做并行 range 切分,还会显示前缀信息:
-> Index scan on t1 using date_col, with range parallel scan (distinct prefix: date_col)
此处的 distinct prefix 将同组行对齐到索引前缀,保证不跨 Worker。

Partition Parallel Scan(分区并行扫描)

按分区分片,各 Worker 扫描不同的分区:
-> Index range scan on p1 using k_a, with partition parallel scan (cost=3.56..3.56 rows=1)
EXPLAIN ANALYZE 中还会显示实际使用的分区数:
-> Table scan on p1, with partition parallel scan (partitions: 4)

子查询预执行标注(Pre-evaluated subqueries / derived tables)

当并行计划中的子查询或派生表(Derived Table)由 Leader 预先执行并将结果缓存后再分发给各 Worker 复用时,Gather 节点下方会附加一行 Pre-evaluated ... 标注。该标注仅出现在最外层 Gather 节点下方,用于提示读者:这些子计划不会被 Worker 重复执行。子查询并行的两种策略(预执行 vs 下推)及触发条件详见 并行子查询执行策略,本节仅介绍其在 EXPLAIN 中的输出形态。
标注种类
标注
含义
Pre-evaluated subqueries: select #N[, select #M ...]
Leader 已预执行并缓存的子查询列表,#N 为子查询的 select 编号
Pre-evaluated derived tables: name1[, name2 ...]
Leader 已预执行并缓存的派生表 / CTE 列表,name 为派生表的别名
若两种标注同时出现,将合并为同一行输出,形如 Pre-evaluated subqueries: select #N, derived tables: name

示例1:预执行子查询

WHERE 子句中的非相关子查询会在外层 Gather 下显示 Pre-evaluated subqueries: select #N
tdsql> EXPLAIN FORMAT=TREE
SELECT /*+ PARALLEL(4) */ *
FROM t1
WHERE t1.b = (SELECT COUNT(*) FROM t2);
EXPLAIN
-> Gather (slice: 1, workers: 4) (cost=4617.95..4640.97 rows=8)
Pre-evaluated subqueries: select #2
-> Filter: (t1.b = (select #2)) (cost=2322.49..2334.47 rows=2)
-> Table scan on t1, with range parallel scan (cost=1.03..20.65 rows=20)
-> Select #2 (subquery in condition; run only once)
-> Aggregate: count(0) (cost=2310.52..2310.52 rows=1)
-> Gather (slice: 1, workers: 4) (cost=2305.54..2309.73 rows=4)
-> Count rows in t2 (cost=5.35..5.35 rows=1)
Index scan on t2 using a, with range parallel scan
阅读要点
外层 GatherPre-evaluated subqueries: select #2 表明 WHERE 里的标量子查询由 Leader 预先执行并将结果缓存。
子查询节点尾部的 subquery in condition; run only once 说明其结果被缓存、仅执行一次。
子查询自身内部也可以使用并行计划(本例中内嵌 Gather (slice: 1, workers: 4) + Count rows in t2)。

示例2:预执行派生表

FROM 子句中的派生表若走预执行路径,会在外层 Gather 下显示 Pre-evaluated derived tables: <alias>
tdsql> EXPLAIN FORMAT=TREE
SELECT /*+ PARALLEL(4) NO_BKA(t1) NO_BNL(t1) */ COUNT(*)
FROM t1 JOIN (SELECT a FROM t2 LIMIT 3) d1 USING(a);
EXPLAIN
-> Aggregate: count(0) (cost=4620.69..4620.69 rows=1)
-> Gather (slice: 1, workers: 4) (cost=4620.29..4620.29 rows=1)
Pre-evaluated derived tables: d1
-> Aggregate: count(0) (cost=2315.85..2315.85 rows=1)
-> Nested loop inner join (cost=2315.39..2315.39 rows=1)
-> Filter: (d1.a is not null) (cost=2309.37..2309.81 rows=3)
-> Table scan on d1 (cost=2309.31..2309.64 rows=3)
-> Materialize (cost=2309.31..2309.64 rows=3)
-> Limit: 3 row(s) (cost=2304.65..2305.67 rows=3)
-> Gather (slice: 1, workers: 4) (cost=2304.65..2306.26 rows=4)
-> Limit: 3 row(s) (cost=0.35..1.04 rows=3)
-> Index scan on t2 using a, with range parallel scan (cost=0.99..19.86 rows=20)
-> Index lookup on t1 using a (a=d1.a), with range parallel scan (cost=1.86..1.86 rows=1)
阅读要点
外层 GatherPre-evaluated derived tables: d1 表明派生表 d1 由 Leader 预先物化,各 Worker 通过 Table scan on d1 直接读取已物化的临时表。
派生表内部同样是一个并行子计划(内嵌 Gather + range parallel scan),该并行子计划由 Leader 执行一次,供外层多个 Worker 共享。

示例3:两种标注同时出现

如果外层查询同时包含预执行子查询和预执行派生表,两种标注会合并在同一行输出:
tdsql> EXPLAIN FORMAT=TREE
SELECT /*+ PARALLEL(4) NO_BKA(t1) NO_BNL(t1) */ COUNT(*)
FROM t1 JOIN (SELECT a FROM t2 LIMIT 3) d1 USING(a)
WHERE t1.b = (SELECT COUNT(*) FROM t2);
EXPLAIN
-> Aggregate: count(0) (cost=11557.69..11557.69 rows=1)
-> Gather (slice: 1, workers: 4) (cost=11557.29..11557.29 rows=1)
Pre-evaluated subqueries: select #3, derived tables: d1
-> Aggregate: count(0) (cost=9252.82..9252.82 rows=1)
-> Nested loop inner join (cost=9252.60..9252.60 rows=1)
-> Filter: (d1.a is not null) (cost=2309.37..2309.81 rows=3)
-> Table scan on d1 (cost=2309.31..2309.64 rows=3)
-> Materialize (cost=2309.31..2309.64 rows=3)
-> Limit: 3 row(s) (cost=2304.65..2305.67 rows=3)
-> Gather (slice: 1, workers: 4) (cost=2304.65..2306.26 rows=4)
-> Limit: 3 row(s) (cost=0.35..1.04 rows=3)
-> Index scan on t2 using a, with range parallel scan (cost=0.99..19.86 rows=20)
-> Filter: (t1.b = (select #3)) (cost=2314.26..2314.26 rows=1)
-> Index lookup on t1 using a (a=d1.a), with range parallel scan (cost=3.71..3.71 rows=1)
-> Select #3 (subquery in condition; run only once)
-> Aggregate: count(0) (cost=2310.52..2310.52 rows=1)
-> Gather (slice: 1, workers: 4) (cost=2305.54..2309.73 rows=4)
-> Count rows in t2 (cost=5.35..5.35 rows=1)
Index scan on t2 using a, with range parallel scan
阅读要点
单行合并格式 Pre-evaluated subqueries: select #3, derived tables: d1 表示子查询 #3 与派生表 d1 均由 Leader 预先执行并缓存。
若某一类为空,则只保留出现的那一段(如仅 Pre-evaluated subqueries: ... 或仅 Pre-evaluated derived tables: ...)。

与下推路径的对照

预执行路径:外层 Gather Pre-evaluated ... 标注 → 子查询/派生表由 Leader 提前执行并缓存,各 Worker 直接读取结果。
下推路径:外层 Gather 上述标注 → 子查询/派生表作为 PartialPlan 的一部分下发到 Worker 内部执行。
完整的路径判别规则与选择条件详见 并行子查询执行策略

完整树形计划示例

以下是一个典型的并行查询树形计划:
tdsql> EXPLAIN FORMAT=TREE SELECT /*+ PARALLEL(4) */ a, COUNT(*) FROM ta GROUP BY a;
EXPLAIN
-> Table scan on <temporary> (cost=2309.23..2309.23 rows=1)
-> Aggregate using temporary table (cost=2309.23..2309.23 rows=1)
-> Gather (slice: 1, workers: 4) (cost=2308.83..2308.83 rows=1)
-> Aggregate: count(0) (cost=4.33..4.33 rows=1)
-> Table scan on ta, with pushed projection, with range parallel scan (cost=3.56..3.56 rows=1)
阅读方法
1. 从最内层(缩进最深)开始阅读:4个 Worker 各自对 ta 表执行 range parallel scan
2. 每个 Worker 在扫描结果上执行 Aggregate: count(0) 局部聚合
3. Gather 节点收集4个 Worker 的结果
4. Leader 侧执行 Aggregate using temporary table 全局聚合
5. 最终通过 Table scan on <temporary> 输出结果

EXPLAIN ANALYZE VERBOSE 执行分析

EXPLAIN ANALYZE 会实际执行查询并收集运行时统计信息。VERBOSE 选项会显示更详细的 per-worker 信息。

基本执行统计

每个节点显示 (actual time=N.NNN..N.NNN rows=N loops=N) 格式的运行时统计:
-> Gather (slice: 1, workers: 4) (cost=3633.69..3651.21 rows=100) (actual time=0.123..0.456 rows=4 loops=1)
-> Table scan on <temporary> (cost=1329.52..1333.52 rows=25) (actual time=0.045..0.089 rows=4 loops=4)
-> Temporary table with deduplication (cost=1329.52..1333.52 rows=25) (actual time=0.040..0.085 rows=4 loops=4)
-> Table scan on t1, with pushed projection, with range parallel scan (ranges: 0) (cost=0.29..737.33 rows=2500) (actual time=0.010..0.050 rows=4 loops=4)
字段含义
字段
说明
actual time=A..B
A = 首行返回时间(ms),B = 末行返回时间(ms)
rows
实际返回的行数
loops
Init/Read 循环次数。Gather 节点位于 Leader 侧,loops 通常为 1;Gather 下方的 PartialPlan 节点由各 Worker 独立执行,loops 通常等于 Worker 数

VERBOSE 模式的 per-worker 信息

添加 VERBOSE 关键字后,每个 PartialPlan 节点下方会显示各 Worker 的独立统计:
tdsql> EXPLAIN ANALYZE VERBOSE SELECT /*+ PARALLEL(4) */ a, COUNT(*) FROM ta GROUP BY a;
EXPLAIN
-> Table scan on <temporary> (cost=X..X rows=X) (actual time=X..X rows=X loops=X)
-> Aggregate using temporary table (cost=X..X rows=X) (actual time=X..X rows=X loops=X)
In memory temporary table size: XkB
-> Gather (slice: X, workers: X) (cost=X..X rows=X) (actual time=X..X rows=X loops=X)
Slice X worker X: 'node-name'
-> Table scan on <temporary> (cost=X..X rows=X) (actual time=X..X rows=X loops=X)
Slice X worker X: (actual time=X..X rows=X loops=X)
-> Aggregate using temporary table (cost=X..X rows=X) (actual time=X..X rows=X loops=X)
In memory temporary table size: XkB
Slice X worker X: (actual time=X..X rows=X loops=X)
-> Table scan on ta, with pushed projection, with range parallel scan (ranges: X) (cost=X..X rows=X) (actual time=X..X rows=X loops=X)
Slice X worker X: (actual time=X..X rows=X loops=X)
关键信息
Slice N worker N: 'node-name':Gather 节点下方显示各 Worker 所在的实际节点名称(如 'node-mtr-xxx-1-001'
Slice N worker N: (actual time=X..X rows=X loops=X):PartialPlan 各节点下方显示各 Worker 的独立耗时和行数
In memory temporary table size: XkB:临时表内存使用量
EXPLAIN ANALYZE VERBOSE 还支持查看计划中特定节点、Worker 的内存、RPC 等更多统计信息,详细请参考 并行计划进阶解读

使用 Optimizer Trace 分析并行计划拒绝原因

Optimizer Trace 可以追踪优化器在并行计划生成过程中的全部决策,下面介绍如何通过 Optimizer Trace 分析并行计划被拒绝的原因。对于 Optimizer Trace 更高级的解读,请参考 并行计划进阶解读

开启 Optimizer Trace

SET optimizer_trace = "enabled=on";
<要分析的查询>;
SELECT TRACE FROM information_schema.OPTIMIZER_TRACE;
并行决策的 trace 位于 JSON 中的 parallel_plan 节点下:
{
"parallel_plan": {
"select#": 1,
"steps": [
{ "considering": { ... } },
{ "generating": { ... } }
]
}
}
其中 considering 是并行计划决策的入口节点。chosen: true 表示采纳了并行计划,chosen: false 表示未采纳(附带 cause 说明原因)。
采纳并行计划chosen: true):
{
"considering": {
"optimization": "...",
"chosen": true
}
}
拒绝并行计划chosen: false):
{
"considering": {
"chosen": false,
"cause": "not_supported_rollup"
}
}
注意:
chosen: true 时包含 optimization 字段,chosen: false 时包含 cause 字段。optimization 字段在默认 trace features 下显示为 "..." 占位符;开启 parallel_plan_optimization=on 后展开为完整 JSON 对象。
例如如下场景:
-- ROLLUP 不支持并行
SELECT /*+ PARALLEL(4) */ a, COUNT(*) FROM t1 GROUP BY a WITH ROLLUP;
Trace 输出:
{
"parallel_plan": {
"select#": 1,
"steps": [
{
"considering": {
"chosen": false,
"cause": "not_supported_rollup"
}
}
]
}
}
如果想要了解更多的并行计划拒绝原因,请参考 并行计划进阶解读