概述
本文介绍如何通过
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 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=TREESELECT /*+ PARALLEL(4) */ *FROM t1WHERE 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=TREESELECT /*+ 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=TREESELECT /*+ 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 scan2. 每个 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: XkBSlice 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:临时表内存使用量使用 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"}}]}}