二、基本并行标识(传统 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 DISTINCT 或 GROUP 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
阅读要点:
·外层 Gather 下 Pre-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)
阅读要点:
·外层 Gather 下 Pre-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 更高级的解读,请参考 并行计划进阶解读。
20、开启 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" } } ] }}
如果想要了解更多的并行计划拒绝原因,请参考 并行计划进阶解读。