TDSQL Boundless 并行查询计划解读

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

相关产品
TDSQL Boundless 产品页,含版本规格、价格与购买入口。

二、基本并行标识(传统 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" } } ] }}

如果想要了解更多的并行计划拒绝原因,请参考 并行计划进阶解读。

赞 (0)
上一篇 4天前
下一篇 4天前