帮你快速理解、总结文档立即下载

并行查询计划进阶解读

最近更新时间:2026-08-21 10:08:30
我的收藏

概述

本文在 并行计划解读 基础上,介绍 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 methodIn memory temporary table sizeChunk 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。不带 VERBOSEEXPLAIN 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 RPCtdsql_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 调用延迟分布。末尾的 SpanTrace_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。不带 VERBOSEEXPLAIN ANALYZE RPC 会被忽略,行为等同于 EXPLAIN ANALYZE
actual rpcs/rpc latencyVERBOSE + 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 sizeSort 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=10GetRowsCount 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_searchrange_optimizerdynamic_rangerepeated_subselect):

parallel_plan_optimization — 并行优化器内部决策

控制 considering 节点中 optimization 字段的内容。
设置
optimization 字段内容
可见的子节点
默认(未开启)
"..." 占位符
parallel_plan_optimization=on
完整 JSON 对象
enumerating_pathschoosing_parallel_scan_tablechosen_parallel_scan_tablepathsadd_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_executionsteps):
{
"serialize_plan": {
"cached_subselects": [],
"serialize_success": true
}
}
记录计划序列化是否成功,以及缓存的子查询信息。
jobs_info 节点join_executionsteps):
{
"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 中并行计划决策相关函数(ChooseParallelPlanparallel_safe 等)。

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

consideringchosen: 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=TREE
SELECT /*+ 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.id
WHERE 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_partssmall_dop 是表级分区扫描特有,不会在 JOIN 级触发。

典型 Optimizer Trace 分析流程

1. 查看 parallel_plan steps considering 节点:确认并行计划是否被采纳
2. 如果 chosen: false:查看 cause,确定拒绝原因
3. 如果 chosen: true:查看 generatingplan_slice 确认 slice 划分和字段下推
4. 检查 generating 中是否有 fallback_to_serial_execution:确认是否回退到串行
5. 特定场景:查看 choosing_parallel_scan_tableauto_dop_abort/cost 等(仅在特定查询类型下出现)