二、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 等(仅在特定查询类型下出现)