概述
本文在 并行计划解读 基础上,介绍 TDSQL Boundless 中如何通过
EXPLAIN ANALYZE VERBOSE 对并行计划进行进一步解读,包括执行统计分析以及如何用 Optimizer Trace 追踪并行决策过程。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...Xnode-X-X=latency(ms): 4,X,X...XPQStartSession=latency(ms): 1,X,X...Xnode-X-X=latency(ms): 1,X,X...XPQStartWorkers=latency(ms): 1,X,X...Xnode-X-X=latency(ms): 1,X,X...XParallelPQSetupRowChannel=latency(ms): 1,X,X...Xnode-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...XSpan: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) | 否 | 否 | 否 | 否 |
注意:
RPC 关键字在内部实现上要求 VERBOSE 同时为 true。不带 VERBOSE 的 EXPLAIN ANALYZE RPC 会被忽略,行为等同于 EXPLAIN ANALYZE。actual rpcs/rpc latency 由 VERBOSE + tdsql_enable_rpc_trace=1 控制,无需 RPC 关键字。迭代器级 RPC 明细(
Leader: 段)和 Performance Traces Summary 由 VERBOSE RPC + tdsql_enable_rpc_trace=1 控制。实际用例
以下通过一组递进的实际用例展示各选项的差异。建表数据:
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-nameSlice 1 worker 1: node-nameSlice 1 worker 2: node-nameSlice 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: 65kBSlice 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.aSlice 1 worker 0: node-nameSlice 1 worker 1: node-nameSlice 1 worker 2: node-nameSlice 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: 33kBSlice 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-nameSlice 1 worker 1: node-nameSlice 1 worker 2: node-nameSlice 1 worker 3: node-nameLeader:PQSetupRowChannel=latency(ms): 4,1.015,0.184...0.308node-X-X=latency(ms): 4,1.015,0.184...0.308PQStartSession=latency(ms): 1,1.059,1.059...1.059node-X-X=latency(ms): 1,1.059,1.059...1.059PQStartWorkers=latency(ms): 1,0.152,0.152...0.152node-X-X=latency(ms): 1,0.152,0.152...0.152ParallelPQSetupRowChannel=latency(ms): 1,0.340,0.340...0.340node-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.340node-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.183Span: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 故不显示 rpcsLeader: 段列出 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-nameSlice 1 worker 1: node-nameSlice 1 worker 2: node-nameSlice 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
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 |
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 数 |
并行决策的关键 Trace 节点
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 等)。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 仅在特定场景下出现(如相关子查询包含下推字段时)。自适应并行度决策过程
如果要观察 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}"
中止原因
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 级触发。典型 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 等(仅在特定查询类型下出现)