TDSQL Boundless 并行查询计划进阶解读

这篇在 并行计划解读 基础上,介绍 TDSQL Boundless 中如何通过 EXPLAIN ANALYZE VERBOSE 对并行计划进行进一步解读,包括执行统计分析以及如何用 Optimizer Trace 追踪并行决策过程。

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

二、EXPLAIN ANALYZE VERBOSE 执行分析

EXPLAIN ANALYZE 会实际执行查询并收集运行时统计信息。VERBOSE 选项会显示更详细的 per-worker 信息。除了 并行计划解读 中介绍的基本信息外,还支持内存、RPC 等信息的统计。

三、内存使用统计

以下内存统计信息仅在 VERBOSE 模式下显示(plain EXPLAIN ANALYZE 不显示):

注意:Merge sort: <cols> 是计划结构描述,在 plain ANALYZE 和 VERBOSE 中都会显示。而 Sort method、In memory temporary table size、Chunk pair files 等属于运行时统计,仅 VERBOSE 模式显示。

排序内存:

-> Sort: t1.a (cost=3472492.60..3472492.60 rows=2500000) (actual time=N.NNN..N.NNN rows=N loops=N) Sort method: std::stable_sort, peak memory used: NNNkB

临时表内存:

-> Aggregate using temporary table (cost=X..X rows=X) (actual time=X..X rows=X loops=X) In memory temporary table size: XkB

Hash Join 内存:

-> Inner hash join (t2.b = t1.a) with join runtime filter: bloom(t2.b) (cost=X..X rows=X) (actual time=X..X rows=X loops=X) Chunk pair files: N, memory usage: NNNkB, join runtime filter size: NNNkB, join runtime filter insert count: NNN

四、RPC 统计

RPC 统计信息分为两个层次,由不同的选项控制:

信息层次 显示条件 说明
actual rpcs / rpc latency(节点级) VERBOSE + tdsql_enable_rpc_trace=1 Gather 等节点的 actual 行中显示 RPC 次数和延迟
Leader: 迭代器级 RPC 明细 VERBOSE RPC + tdsql_enable_rpc_trace=1 Gather 节点下方显示各 RPC 调用的详细延迟
Performance Traces Summary VERBOSE RPC EXPLAIN 输出末尾的全局 RPC trace 摘要

注意:说明:

注意:tdsql_enable_rpc_trace 默认为 true(开启),用户通常无需显式设置即可看到 RPC 统计;下文用例中的 SET tdsql_enable_rpc_trace = 1/0 是为演示对比而显式设置。

注意:RPC 关键字在内部实现上要求 VERBOSE 同时为 true。不带 VERBOSE 的 EXPLAIN ANALYZE RPC 会被忽略,行为等同于 EXPLAIN ANALYZE。

五、节点级 RPC 统计

当 VERBOSE 开启且 tdsql_enable_rpc_trace=1 时,节点的 actual 行会额外显示 RPC 次数和延迟(无需 RPC 关键字)。actual rpcs 不仅出现在 Gather 节点上,也出现在所有发起 RPC 调用的节点上(如 Count rows in t1);在 per-worker 行中,仅对实际发起 RPC 的 Worker 显示:

-> Gather (slice: 1, workers: 4) (cost=X..X rows=N) (actual time=N.NNN..N.NNN rows=N rpcs=7 rpc latency=2.030 loops=N) Slice 1 worker 0: 'node-name' … -> Count rows in t1 (cost=X..X rows=N) (actual time=N.NNN..N.NNN rows=N rpcs=10 rpc latency=3.419 loops=N) Table scan on t1, with pushed condition: (t1.a > 0), projection, with range parallel scan (ranges: 0) Slice 1 worker 0: (actual time=N.NNN..N.NNN rows=N loops=N) Slice 1 worker 2: (actual time=N.NNN..N.NNN rows=N rpcs=41 rpc latency=13.676 loops=N) Slice 1 worker 3: (actual time=N.NNN..N.NNN rows=N loops=N)

上例中,Count rows in t1 节点发起 GetRowsCount RPC,因此也显示 rpcs=10;在 per-worker 行中,仅 worker 2 实际执行了 RPC 调用(rpcs=41),其他 Worker 无 RPC 调用故不显示。

字段 说明
actual rpcs=N 平均每次 loop 的 RPC 调用次数
rpc latency=N.NNN 平均每次 loop 的 RPC 总延迟(ms)

六、迭代器级 RPC 明细

当同时使用 VERBOSE RPC 且 tdsql_enable_rpc_trace=1 时,Gather 节点下方会显示详细的 RPC 调用链路:

-> Gather (slice: 1, workers: 4) (cost=X..X rows=X) (actual time=X..X rows=X actual rpcs=X rpc latency=X loops=X) Slice 1 worker 0: 'node-X-X' Slice 1 worker 1: 'node-X-X' Leader: PQSetupRowChannel=latency(ms): 4,X,X…X node-X-X=latency(ms): 4,X,X…X PQStartSession=latency(ms): 1,X,X…X node-X-X=latency(ms): 1,X,X…X PQStartWorkers=latency(ms): 1,X,X…X node-X-X=latency(ms): 1,X,X…X ParallelPQSetupRowChannel=latency(ms): 1,X,X…X node-X-X=latency(ms): 1,X,X…X

RPC 调用类型:

RPC 名称 说明
PQSetupRowChannel 建立 Worker 与 Leader 之间的行通道
ParallelPQSetupRowChannel 批量并行建立行通道
PQStartSession 启动远端并行会话
PQStartWorkers 启动远端 Worker
GetSmallRanges 获取数据分片范围
GetRowsCount 获取行计数(用于 COUNT(*) 下推)
LocalScanRecord 本地扫描记录传输

七、Performance Traces Summary

当使用 VERBOSE RPC 时,EXPLAIN 输出末尾会附加全局 RPC trace 摘要:

Performance Traces Summary: leader: -> PQSetupRowChannel=latency(ms): X,X,X…X -> node-X-X=latency(ms): X,X,X…X -> PQStartSession=latency(ms): X,X,X…X -> node-X-X=latency(ms): X,X,X…X -> PQStartWorkers=latency(ms): X,X,X…X -> node-X-X=latency(ms): X,X,X…X -> ParallelPQSetupRowChannel=latency(ms): X,X,X…X -> node-X-X=latency(ms): X,X,X…X 'node-X-X': -> GetRowsCount=latency(ms): X,X,X…X -> node-X-X=latency(ms): X,X,X…X -> GetSmallRanges=latency(ms): X,X,X…X -> node-X-X=latency(ms): X,X,X…X Span: Trace_id: X

该摘要按节点(Leader / Worker 节点)分组,展示各节点上的 RPC 调用延迟分布。末尾的 Span 和 Trace_id 为全局 trace 标识。

注意:若 tdsql_enable_rpc_trace=0,Performance Traces Summary 仅显示 Span: 和 Trace_id:,不显示具体 RPC 调用延迟。

八、语法汇总

# 基本执行分析EXPLAIN ANALYZE SELECT …# 显示 per-worker 详细信息EXPLAIN ANALYZE VERBOSE SELECT …# 显示 RPC 统计(需要同时开启 VERBOSE)EXPLAIN ANALYZE VERBOSE RPC SELECT …

各选项行为说明:

语法 per-worker 信息 actual rpcs/rpc latency 迭代器级 RPC 明细 Performance Traces Summary
EXPLAIN ANALYZE 否 否 否 否
EXPLAIN ANALYZE VERBOSE 是 是(需 tdsql_enable_rpc_trace=1) 否 否
EXPLAIN ANALYZE VERBOSE RPC 是 是(需 tdsql_enable_rpc_trace=1) 是(需 tdsql_enable_rpc_trace=1) 是
EXPLAIN ANALYZE RPC(不带 VERBOSE) 否 否 否 否

九、实际用例

以下通过一组递进的实际用例展示各选项的差异。建表数据:

CREATE TABLE t1 (a int, b int, c varchar(20));INSERT INTO t1 VALUES (1,10,'aaa'),(2,20,'bbb'),(3,30,'ccc'),(4,40,'ddd'),(5,50,'eee');– 插入更多数据使并行有意义INSERT INTO t1 SELECT a+5, b+10, c FROM t1;INSERT INTO t1 SELECT a+10, b+20, c FROM t1;INSERT INTO t1 SELECT a+20, b+40, c FROM t1;ANALYZE TABLE t1;

actual time 因每次运行不同以 N.NNN..N.NNN 表示,节点名以 node-name 表示,其余数值均为真实值。

十、用例1:EXPLAIN ANALYZE(基本执行统计)

EXPLAIN ANALYZE SELECT /*+ PARALLEL(4) */ a, COUNT(*) FROM t1 GROUP BY a;

-> Table scan on <temporary> (cost=4806.27..4822.77 rows=100) (actual time=N.NNN..N.NNN rows=40 loops=1) -> Aggregate using temporary table (cost=4806.27..4822.77 rows=100) (actual time=N.NNN..N.NNN rows=40 loops=1) -> Gather (slice: 1, workers: 4) (cost=4668.85..4686.37 rows=100) (actual time=N.NNN..N.NNN rows=40 loops=1) -> Table scan on <temporary> (cost=2364.68..2368.68 rows=25) (actual time=N.NNN..N.NNN rows=10 loops=4) -> Aggregate using temporary table (cost=2364.68..2368.68 rows=25) (actual time=N.NNN..N.NNN rows=10 loops=4) -> Table scan on t1, with pushed projection, with range parallel scan (ranges: 0) (cost=0.50..1244.24 rows=2500) (actual time=N.NNN..N.NNN rows=10 loops=4)

观察:

·Gather loops=1(Leader 侧执行一次),PartialPlan 节点 loops=4(4 个 Worker 各执行一次)

·PartialPlan 中 Table scan on t1 实际 rows=10(每个 Worker 扫描 10 行),Leader 侧 rows=40(4 个 Worker 汇总 40 行)

·不显示 In memory temporary table size、Sort method、per-worker 行(这些需要 VERBOSE)

十一、用例2:EXPLAIN ANALYZE VERBOSE(per-worker + 内存统计)

EXPLAIN ANALYZE VERBOSE SELECT /*+ PARALLEL(4) */ a, COUNT(*) FROM t1 GROUP BY a;

-> Table scan on <temporary> (cost=4806.27..4822.77 rows=100) (actual time=N.NNN..N.NNN rows=40 loops=1) -> Aggregate using temporary table (cost=4806.27..4822.77 rows=100) (actual time=N.NNN..N.NNN rows=40 loops=1) In memory temporary table size: 65kB -> Gather (slice: 1, workers: 4) (cost=4668.85..4686.37 rows=100) (actual time=N.NNN..N.NNN rows=40 loops=1) Slice 1 worker 0: node-name Slice 1 worker 1: node-name Slice 1 worker 2: node-name Slice 1 worker 3: node-name -> Table scan on <temporary> (cost=2364.68..2368.68 rows=25) (actual time=N.NNN..N.NNN rows=10 loops=4) Slice 1 worker 0: (actual time=N.NNN..N.NNN rows=40 loops=1) Slice 1 worker 1: (actual time=N.NNN..N.NNN rows=0 loops=1) Slice 1 worker 2: (actual time=N.NNN..N.NNN rows=0 loops=1) Slice 1 worker 3: (actual time=N.NNN..N.NNN rows=0 loops=1) -> Aggregate using temporary table (cost=2364.68..2368.68 rows=25) (actual time=N.NNN..N.NNN rows=10 loops=4) In memory temporary table size: 65kB Slice 1 worker 0: (actual time=N.NNN..N.NNN rows=40 loops=1) Slice 1 worker 1: (actual time=N.NNN..N.NNN rows=0 loops=1) Slice 1 worker 2: (actual time=N.NNN..N.NNN rows=0 loops=1) Slice 1 worker 3: (actual time=N.NNN..N.NNN rows=0 loops=1) -> Table scan on t1, with pushed projection, with range parallel scan (ranges: 0) (cost=0.50..1244.24 rows=2500) (actual time=N.NNN..N.NNN rows=10 loops=4) Slice 1 worker 0: (actual time=N.NNN..N.NNN rows=40 loops=1) Slice 1 worker 1: (actual time=N.NNN..N.NNN rows=0 loops=1) Slice 1 worker 2: (actual time=N.NNN..N.NNN rows=0 loops=1) Slice 1 worker 3: (actual time=N.NNN..N.NNN rows=0 loops=1)

观察:

·VERBOSE 新增了 In memory temporary table size: 65kB(临时表内存)和 Slice N worker N 行

·每个 PartialPlan 节点下方都有 per-worker 的独立耗时统计

·数据倾斜可见:worker 0 处理了全部40行(rows=40),其余 Worker 处理0行 — 说明 range parallel scan 的数据分片不均匀

十二、用例3:EXPLAIN ANALYZE VERBOSE + ORDER BY(Merge sort + Sort method)

EXPLAIN ANALYZE VERBOSE SELECT /*+ PARALLEL(4) */ a FROM t1 ORDER BY a;

-> Gather (slice: 1, workers: 4) (cost=5324.32..9319.77 rows=10000) (actual time=N.NNN..N.NNN rows=40 loops=1) Merge sort: t1.a Slice 1 worker 0: node-name Slice 1 worker 1: node-name Slice 1 worker 2: node-name Slice 1 worker 3: node-name -> Sort: t1.a (cost=3018.59..3018.59 rows=2500) (actual time=N.NNN..N.NNN rows=10 loops=4) Sort method: std::sort, peak memory used: 33kB Slice 1 worker 0: (actual time=N.NNN..N.NNN rows=0 loops=1) Slice 1 worker 1: (actual time=N.NNN..N.NNN rows=0 loops=1) Slice 1 worker 2: (actual time=N.NNN..N.NNN rows=40 loops=1) Slice 1 worker 3: (actual time=N.NNN..N.NNN rows=0 loops=1) -> Table scan on t1, with pushed projection, with range parallel scan (ranges: 0) (cost=0.50..1244.24 rows=2500) (actual time=N.NNN..N.NNN rows=10 loops=4) Slice 1 worker 0: (actual time=N.NNN..N.NNN rows=0 loops=1) Slice 1 worker 1: (actual time=N.NNN..N.NNN rows=0 loops=1) Slice 1 worker 2: (actual time=N.NNN..N.NNN rows=40 loops=1) Slice 1 worker 3: (actual time=N.NNN..N.NNN rows=0 loops=1)

观察:

·Merge sort: t1.a 在 plain ANALYZE 和 VERBOSE 中都显示(计划结构,非运行时统计)

·Sort method: std::sort, peak memory used: 33kB 仅在 VERBOSE 中显示(运行时统计)

·排序节点 loops=4(4个 Worker 各排一次),Leader 侧 Gather loops=1(归并一次)

十三、用例4:EXPLAIN ANALYZE VERBOSE RPC(RPC 统计 + 迭代器级明细)

SET tdsql_enable_rpc_trace = 1;EXPLAIN ANALYZE VERBOSE RPC SELECT /*+ PARALLEL(4) */ COUNT(*) FROM t1 WHERE a > 0;

-> Aggregate: count(0) (cost=3549.45..3549.45 rows=1) (actual time=N.NNN..N.NNN rows=1 loops=1) -> Gather (slice: 1, workers: 4) (cost=2615.28..3548.67 rows=4) (actual time=N.NNN..N.NNN rows=4 rpcs=7 rpc latency=2.566 loops=1) Slice 1 worker 0: node-name Slice 1 worker 1: node-name Slice 1 worker 2: node-name Slice 1 worker 3: node-name Leader: PQSetupRowChannel=latency(ms): 4,1.015,0.184…0.308 node-X-X=latency(ms): 4,1.015,0.184…0.308 PQStartSession=latency(ms): 1,1.059,1.059…1.059 node-X-X=latency(ms): 1,1.059,1.059…1.059 PQStartWorkers=latency(ms): 1,0.152,0.152…0.152 node-X-X=latency(ms): 1,0.152,0.152…0.152 ParallelPQSetupRowChannel=latency(ms): 1,0.340,0.340…0.340 node-X-X=latency(ms): 1,0.340,0.340…0.340 -> Count rows in t1 (cost=1244.24..1244.24 rows=1) (actual time=N.NNN..N.NNN rows=1 rpcs=10 rpc latency=3.531 loops=4) Table scan on t1, with pushed condition: (t1.a > 0), projection, with range parallel scan (ranges: 0) Slice 1 worker 0: (actual time=N.NNN..N.NNN rows=1 rpcs=41 rpc latency=14.123 loops=1) Slice 1 worker 1: (actual time=N.NNN..N.NNN rows=1 loops=1) Slice 1 worker 2: (actual time=N.NNN..N.NNN rows=1 loops=1) Slice 1 worker 3: (actual time=N.NNN..N.NNN rows=1 loops=1)Performance Traces Summary: leader: -> PQSetupRowChannel=latency(ms): 4,1.015,0.184…0.308 -> node-X-X=latency(ms): 4,1.015,0.184…0.308 -> PQStartSession=latency(ms): 1,1.059,1.059…1.059 -> node-X-X=latency(ms): 1,1.059,1.059…1.059 -> PQStartWorkers=latency(ms): 1,0.152,0.152…0.152 -> node-X-X=latency(ms): 1,0.152,0.152…0.152 -> ParallelPQSetupRowChannel=latency(ms): 1,0.340,0.340…0.340 -> node-X-X=latency(ms): 1,0.340,0.340…0.340 node-name: -> GetRowsCount=latency(ms): 41,14.123,0.315…0.541 -> node-X-X=latency(ms): 41,14.123,0.315…0.541 -> GetSmallRanges=latency(ms): 1,0.183,0.183…0.183 -> node-X-X=latency(ms): 1,0.183,0.183…0.183 Span: Trace_id: X

观察:

·Gather 节点 rpcs=7 对应 Leader 侧4类 RPC 调用次数之和(PQSetupRowChannel 4 + PQStartSession 1 + PQStartWorkers 1 + ParallelPQSetupRowChannel 1 = 7),与下方 Leader: 段的调用次数吻合

·Count rows in t1 节点 rpcs=10(GetRowsCount RPC)

·per-worker 行中,仅 worker 0 显示 rpcs=41 rpc latency=14.123(它实际执行了数据扫描 RPC),其他 Worker 无 RPC 故不显示 rpcs

·Leader: 段列出 Leader 发起的4类 RPC 调用及延迟(格式:调用次数,总延迟,最小延迟…最大延迟)

·Performance Traces Summary 按节点分组:leader 段列出 Leader 侧 RPC,node-name 段列出 Worker 侧 RPC(GetRowsCount 41次调用、GetSmallRanges 1次调用)

·RPC 延迟格式为 次数,总延迟 ms,最小延迟 ms…最大延迟 ms

十四、用例5:EXPLAIN ANALYZE RPC(不带 VERBOSE — 被忽略)

EXPLAIN ANALYZE RPC SELECT /*+ PARALLEL(4) */ COUNT(*) FROM t1 WHERE a > 0;

-> Aggregate: count(0) (cost=3549.45..3549.45 rows=1) (actual time=N.NNN..N.NNN rows=1 loops=1) -> Gather (slice: 1, workers: 4) (cost=2615.28..3548.67 rows=4) (actual time=N.NNN..N.NNN rows=4 loops=1) -> Count rows in t1 (cost=1244.24..1244.24 rows=1) (actual time=N.NNN..N.NNN rows=1 loops=4) Table scan on t1, with pushed condition: (t1.a > 0), projection, with range parallel scan (ranges: 0)

观察:

·输出与 EXPLAIN ANALYZE 完全一致 — RPC 关键字被忽略

·无 per-worker 信息、无 actual rpcs、无 Leader: 段、无 Performance Traces Summary

十五、用例6:EXPLAIN ANALYZE VERBOSE RPC + tdsql_enable_rpc_trace=0

SET tdsql_enable_rpc_trace = 0;EXPLAIN ANALYZE VERBOSE RPC SELECT /*+ PARALLEL(4) */ COUNT(*) FROM t1 WHERE a > 0;

-> Aggregate: count(0) (cost=3549.45..3549.45 rows=1) (actual time=N.NNN..N.NNN rows=1 loops=1) -> Gather (slice: 1, workers: 4) (cost=2615.28..3548.67 rows=4) (actual time=N.NNN..N.NNN rows=4 loops=1) Slice 1 worker 0: node-name Slice 1 worker 1: node-name Slice 1 worker 2: node-name Slice 1 worker 3: node-name -> Count rows in t1 (cost=1244.24..1244.24 rows=1) (actual time=N.NNN..N.NNN rows=1 loops=4) Table scan on t1, with pushed condition: (t1.a > 0), projection, with range parallel scan (ranges: 0) Slice 1 worker 0: (actual time=N.NNN..N.NNN rows=1 loops=1) Slice 1 worker 1: (actual time=N.NNN..N.NNN rows=1 loops=1) Slice 1 worker 2: (actual time=N.NNN..N.NNN rows=1 loops=1) Slice 1 worker 3: (actual time=N.NNN..N.NNN rows=1 loops=1)Performance Traces Summary: Span: Trace_id: X

观察:

·VERBOSE 的 per-worker 信息正常显示(Slice N worker N 行)

·无 actual rpcs/rpc latency(因为 tdsql_enable_rpc_trace=0,未收集 RPC trace)

·无 Leader: 段(RPC trace 为空)

·Performance Traces Summary 仅显示 Span: 和 Trace_id:,不显示具体 RPC 调用延迟

十六、使用 Optimizer Trace 追踪并行决策过程

Optimizer Trace 可以追踪优化器在并行计划生成过程中的全部决策,除了 并行计划解读 中介绍的并行计划拒绝原因,还可以分析并行计划生成中的路径枚举、Slice 切分、自适应并行 等过程。

十七、开启 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": { … } } ] }}

十八、optimizer_trace_features 与并行 trace

optimizer_trace_features 系统变量控制哪些优化器阶段的 trace 被输出。与并行查询相关的有两个 feature,默认均未开启(default_features 仅包含 greedy_search、range_optimizer、dynamic_range、repeated_subselect):

十九、parallel_plan_optimization — 并行优化器内部决策

控制 considering 节点中 optimization 字段的内容。

设置 optimization 字段内容 可见的子节点
默认(未开启) "…" 占位符 无
parallel_plan_optimization=on 完整 JSON 对象 enumerating_paths、choosing_parallel_scan_table、chosen_parallel_scan_table、paths、add_collector_to_final_paths

SET optimizer_trace_features = "default,parallel_plan_optimization=on";

开启后的完整 optimization 对象:

{ "optimization": { "enumerating_paths": { "unqualified_count": { "equivalence_area": 0, "choosing_parallel_scan_table": [ { "table": "`t1`", "usable": true } ], "chosen_parallel_scan_table": { "table": "`t1`" }, "paths": [ "id=0, type=final_singleton, cost=4977.0, rows=1.0", "id=1, type=final_singleton, subpath={?: gather}, cost=3549.5, rows=1.0" ] } } }, "add_collector_to_final_paths": []}

optimization 对象包含的子节点:

子节点 说明 出现条件
enumerating_paths 并行路径探索主入口 总是(当 feature 开启时)
unqualified_count unqualified count 查询的表选择 SELECT COUNT(*) 类查询
choosing_parallel_scan_table 候选并行扫描表列表,含 table/usable/hinted_dop unqualified count 路径
chosen_parallel_scan_table 最终选择的并行扫描表 unqualified count 路径
paths 探索的并行路径列表,每条含 id/type/cost/rows 总是
add_collector_to_final_paths 将 Collector 添加到最终路径的操作 总是

paths 条目中的路径信息格式为 id=N, type=<分布类型>, cost=N.N, rows=N.N。当路径有 auto DOP 相关信息时,还会附加 auto_dop={cost=N.N, dop=N} 或 auto_dop={abort='原因'}。部分路径还会附加 dop_adj=<source> 表示 DOP 可调整来源(s 表示 self,o 表示 outer,i 表示 inner)。

choosing_parallel_scan_table 中的 hinted_dop 字段(仅当表有 PARALLEL/NO_PARALLEL hint 时出现):

值 说明
no_parallel 表被标记为 NO_PARALLEL
auto 自动 DOP(PARALLEL(table AUTO))
数字 显式指定的 DOP
default_value 使用默认 DOP

20、parallel_plan_serialize — 计划序列化与 Job 分配

控制 EXPLAIN(非 EXPLAIN ANALYZE)时是否模拟计划序列化和 job 分配过程。

SET optimizer_trace_features = "default,parallel_plan_serialize=on";EXPLAIN SELECT /*+ PARALLEL(4) */ COUNT(*) FROM t1 WHERE a > 0;SELECT TRACE FROM information_schema.OPTIMIZER_TRACE;

场景 serialize_plan / jobs_info 是否出现 条件
实际执行查询(SELECT) 总是出现(在 join_execution 节点中) 无需任何 feature
EXPLAIN(非 ANALYZE) 仅当 parallel_plan_serialize=on 时出现 

serialize_plan 节点(join_execution → steps):

{ "serialize_plan": { "cached_subselects": [], "serialize_success": true }}

记录计划序列化是否成功,以及缓存的子查询信息。

jobs_info 节点(join_execution → steps):

{ "jobs_info": { "exec_nodes": 1, "all workers": 4, "nodes": [ { "local": false, "remote_jobs": 1, "node_name": "node-mtr-xxx-1-001", "worker_num": 4 } ] }}

记录 job 分配详情:

字段 说明
exec_nodes 执行节点数
all workers 总 Worker 数
nodes 节点列表
nodes[].local 是否为本地节点
nodes[].remote_jobs / local_jobs 远程/本地 job 数
nodes[].node_name 节点名称
nodes[].worker_num 该节点的 Worker 数

22、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" } } ] }}

常见拒绝原因(cause):

cause 说明
disabled_by_zero_of_max_parallel_workers max_parallel_workers 设置为 0,并行查询被禁用
disabled_by_system_variable 系统变量禁用并行
disabled_by_hint NO_PARALLEL hint 禁用并行
not_supported_uncacheable_subquery 不可缓存的子查询不支持并行
disabled_by_subquery_inline_evaluation_hint 子查询内联求值 hint 禁用了并行
not_supported_rollup 不支持 ROLLUP
IN_subquery_strategy_is_SUBQ_EXISTS IN 子查询使用 SUBQ_EXISTS 策略,不支持并行
no_available_parallel_plan 无可用的并行计划
too_many_parallel_workers Worker 数超出限制
item_referred_by_upper_query_block 上层 query block 引用了本层 item

注意:说明:

注意:以上 cause 值来自 sql/parallel_query/planner.cc 中并行计划决策相关函数(ChooseParallelPlan 及 parallel_safe 等)。

23、generating 节点 — 计划生成与 Slice 划分

当 considering 的 chosen: true 时,trace 会继续进入 generating 阶段,记录 slice 划分和字段下推情况:

SELECT /*+ PARALLEL(4) */ COUNT(*) FROM t1 WHERE a > 0;

Trace 输出:

{ "generating": { "plan_slice": [ { "slice": 0, "child_slices": [ { "slice": 1, "tables": [ "`t1`" ], "fields": [ "count(0)" ] } ] } ] }}

字段含义:

字段 说明
slice: 0 根 slice(Leader 侧)
child_slices 子 slice 列表(Worker 侧执行的 PartialPlan)
slice: 1 Worker slice 编号
tables 该 slice 中涉及的表
fields 下推到该 slice 的字段/表达式

如果生成过程中决定回退到串行,generating 节点会显示:

{ "generating": { "fallback_to_serial_execution": true, "fallback_cause": "correlated_subquery_includes_pushdown_fields" }}

注意:说明:

注意:fallback_to_serial_execution 仅在特定场景下出现(如相关子查询包含下推字段时)。

24、自适应并行度决策过程

如果要观察 JOIN 级 自适应并行度(Auto DOP) 决策过程,建议在需要时关闭 Hash Join 相关开关,让执行计划落到 Nested Loop 或 BKA 路径上,例如:

SET optimizer_switch = 'hash_join=off,cost_based_hashjoin=off,block_nested_loop=off,batched_key_access=off,mrr_cost_based=off,pk_preload_pushdown=off';EXPLAIN FORMAT=TREESELECT /*+ QB_NAME(qb_join_auto) PARALLEL(@qb_join_auto auto) PARALLEL(t1 PARTITION) PARALLEL(t2 PARTITION) */ COUNT(*)FROM t1 STRAIGHT_JOIN t2 ON t1.id = t2.idWHERE t1.id >= 0;SELECT * FROM information_schema.optimizer_trace \\G

可以重点关注以下字段:

字段 含义 出现条件
auto_dop_cost 表级 Auto DOP 计算时的串行扫描 cost(serial cost) 表级 Auto DOP 触发时
proposed_dop 当前 path 最终采用的 DOP,无论 Auto DOP 是否成功都会输出 总是(trace_curpath 中无条件添加)
auto_dop_abort 表级 Auto DOP 放弃继续计算的原因 表级 Auto DOP 未继续提升时
auto_dop={cost=…, dop=…} JOIN 路径的输入 cost 和提升后的 DOP JOIN 级 Auto DOP
auto_dop={abort='…'} JOIN 级 Auto DOP 没有继续提升时的原因 JOIN 级未继续提升时
dop_adj DOP 调整来源:s 表示 self(非 JOIN 的透传节点),o 表示 outer(JOIN 路径且 outer 侧有 dop 源),i 表示 inner JOIN / 透传路径

表级 Auto DOP 成功时的 trace 片段(具体数值会随统计信息变化):

"auto_dop_cost": 3119.74,"proposed_dop": 2,"type": "partial_scan","cost": 1559.87

表级 Auto DOP 未继续提升时的 trace 片段:

"auto_dop_abort": "cost_below_threshold","proposed_dop": 1,"type": "partial_scan"

JOIN 级 Auto DOP 的信息附着在 parallel plan path 字符串上,示例如下:

"id=3, type=partial(5), key=2, outer=1, inner=3, cost=702014.1, rows=32308.6, dop_adj=i, auto_dop={cost=2799304.6, dop=4}"

25、中止原因

Auto DOP 的中止原因在 trace 中有两种输出形式:

·独立字段 auto_dop_abort(表级 trace,simplify=false 路径)使用长名:

长名 说明
cost_below_threshold 串行扫描代价低于阈值,无需提升 DOP
already_at_dop_limit 已达 DOP 上限
no_rows 无行数信息
no_partitions 无分区(分区扫描特有)
dop_limit_too_small DOP 上限过小(< 2)

·paths 条目中的 auto_dop={abort='…'}(JOIN 级 trace,simplify=true 路径)使用短名:

短名 对应长名 说明
low_cost cost_below_threshold 代价低于阈值
reach_limit already_at_dop_limit 已达 DOP 上限
no_rows no_rows 无行数信息
no_parts no_partitions 无分区
small_dop dop_limit_too_small DOP 上限过小

需要特别说明的是,JOIN 级 Auto DOP 实际只会触发前三项(low_cost / reach_limit / no_rows),no_parts 与 small_dop 是表级分区扫描特有,不会在 JOIN 级触发。

26、典型 Optimizer Trace 分析流程

1. 查看 parallel_plan → steps → considering 节点:确认并行计划是否被采纳

2. 如果 chosen: false:查看 cause,确定拒绝原因

3. 如果 chosen: true:查看 generating → plan_slice 确认 slice 划分和字段下推

4. 检查 generating 中是否有 fallback_to_serial_execution:确认是否回退到串行

5. 特定场景:查看 choosing_parallel_scan_table、auto_dop_abort/cost 等(仅在特定查询类型下出现)

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