概述
TDSQL Boundless 的查询优化分两层:
串行优化器(默认使用传统贪心优化器):决定表与表怎么连接、用哪种访问方法(全表扫描 / 索引扫描 / ref 等)、连接顺序如何排列。它产出一棵串行执行计划树。
并行查询优化器:在串行计划树之上做"二次优化"——不改连接顺序,而是为这棵已定形的树决定"怎么并行执行最省":哪些表走并行扫描、用多大并行度(DOP)、聚合/排序/limit 是否拆成两阶段、Leader 与 Worker 各做什么。
换句话说:
优化器 | 回答的问题 | 输入 | 输出 |
串行优化器 | 这些表怎么连、用什么访问方法最省? | 查询块 | 一棵串行执行计划树 |
并行查询优化器 | 这棵树怎么并行执行最省? | 串行执行计划树 | Leader Slice + Worker Slice 的并行执行计划 |
并行执行模型
Leader / Worker / Slice
一条并行查询计划被切分为若干 Slice:
Leader Slice(顶层):在主连接上执行,DOP 恒为 1,负责汇总、做无法下推的最终算子。
Worker Slice:由若干 Worker 并行执行同一份 PartialPlan 模板,每个 Worker 处理一部分数据,DOP = Worker 数。
Slice 的边界是 Collector:Worker 产出部分结果,典型的 Gather Collector 把多个 Worker 的流汇聚,上交给 Leader。一个 Collector 把计划切分为上下两片。
说明:
Collector 支持 Gather/Shuffle/Broadcast 等算子,当前版本只有 Gather 启用。
分布类型
并行优化器为每个计划节点选择一种分布类型,描述该节点的数据如何分布到 Worker:
分布类型 | 含义 | DOP |
Leader 单点(final_singleton) | 只在 Leader 执行,最终汇总点 | 1 |
广播(replicated) | 每个 Worker 持有全量副本 | 1 |
并行扫描(partial_scan) | 每个 Worker 扫一片数据(动态范围或分区并行扫描),无重分区 | 可放大 |
按分片键分布(hashed) | 按分布键重分区,或利用物理分片键就地分布;DOP 取决于分布键与物理分片配置 | 取决于体系 |
这些类型决定数据如何流转:Leader 单点需要 Collector 回收;广播等价于每个 Worker 自带全量;并行扫描是本地并行无重分区;按分片键分布则可利用 co-located(同分布)特性避免 shuffle。
两阶段算子
某些算子可以拆成"Worker 做部分、Leader 做剩余"的两阶段形式:
算子 | 两阶段做法 |
聚合(GROUP BY) | Worker 各做局部聚合(如局部 SUM/COUNT),Leader 再合并(SUM 各 Worker 的 SUM) |
排序(ORDER BY) | Worker 各自局部排序,Leader 端做归并排序合并有序流 |
LIMIT | Worker 各取 limit 行(offset 清零),Leader 再做 limit+offset |
窗口函数 | 仅当 PARTITION BY 列与数据分布兼容时下推(每个 Worker 处理完整分区) |
COUNT(*) | Worker 各 COUNT,Leader SUM |
不是所有算子都能两阶段:例如带 DISTINCT 的聚合、不可两阶段的 GROUP_CONCAT / LISTAGG 必须走一阶段(Leader 聚合),因为它们的中间态无法简单合并。
并行代价计算原理
并行优化器为每条候选路径计算代价,自底向上累加。与串行代价相比,并行代价多了三部分并行专属开销。
代价组成
一条并行路径的总代价大致是:
并行代价 = 子算子代价 + 本算子自代价 + 并行专属开销
并行专属开销 = 收集代价(Collector)+ 通信代价(随 DOP² 增长)+ 并行效率折扣
收集代价(Collector)
Collector 把多个 Worker 的流汇聚到 Leader,是并行代价中最核心的部分。
说明:
Collector 支持 Gather/Shuffle/Broadcast 等算子,当前版本只有 Gather 启用。
Gather 由三段组成:
Leader 接收代价:Leader 每接收一行付出固定代价,与总接收行数 N 成正比(按字段数计费)。
Worker 发送代价:每个 Worker 每发送一行付出代价,与单 Worker 行数 N/DOP 成正比(按字段数与行宽字节数计费),再加上子算子的执行代价。
归并排序代价:仅当子路径带序(排序下推)时才产生,Leader 把多个有序流归并,与总行数 N 成正比。
通信代价(抑制过宽并行)
通信代价 = 常数 × DOP²
代价随 DOP 平方增长,作为 all-to-all 通信的保守上界。由于 shuffle 尚未完整支持,这一项用于防止优化器偏向过宽的并行计划——DOP 翻倍,通信代价变四倍,从而抑制盲目增大并行度。
并行效率折扣(小表并行收益低)
并非所有数据都适合并行扫描。对行数少的表,并行扫描收益很小,甚至因为数据倾斜而劣化。并行优化器用一条逻辑斯蒂曲线把有效并行度从"串行级 1/DOP"平滑过渡到"满并行 1.0":
并行效率 = 1/DOP + (1 − 1/DOP) × 逻辑斯蒂函数(去重行数, DOP, 行数缩放因子)
有效 DOP(规划并行度)= DOP × 并行效率 (结果钳制到 ≥ 1)
含义:
去重行数很少时,并行效率趋近 1/DOP(接近串行)——并行几乎无收益。
去重行数充足时,并行效率趋近 1.0(充分并行)。
rows_scale 由会话变量 parallel_plan_efficiency_rows_scale(默认 100)控制:值越大,模型越早认为"行数够多、可以充分并行";设为 0 则禁用该模型(一律按满并行估算)。路径枚举与选择原理
枚举什么
关键区别:串行优化器枚举的是连接顺序;并行优化器不重新枚举连接顺序,它接受串行已选定的计划树,为每个节点枚举"分布方案 + 是否两阶段 + DOP"。
枚举对象是:每个计划节点的多种并行实现方案。例如一个表扫描节点,可枚举"Leader 单点扫描 / 广播 / 动态范围并行扫描 / 分区并行扫描"几种方案;一个聚合节点可枚举"一阶段 Leader 聚合 / 下推一阶段 / 两阶段"。
枚举内容
节点类型 | 枚举的并行方案 |
表扫描 | Leader 单点(通常作为基线路径由优化器提议)/ 广播 / 动态范围并行扫描 / 分区并行扫描。后两者受行数与代价阈值剪枝 |
连接(join) | 同分布直接拼接(partition-wise)/ 一侧广播 / 兜底 Collector 到 Leader /(条件满足时)并行 Hash Join |
聚合 | 一阶段 Collector / 下推一阶段(分布键兼容时)/ 两阶段 |
排序 | 下推排序 + Leader 归并 / 不下推 Collector |
limit | 两阶段(Worker 取 limit、Leader 取 limit+offset) |
窗口 | 分布键兼容时下推,否则 Collector |
支配剪枝(保留非劣解)
一个计划节点可能枚举出多条候选路径。并行优化器用多维支配比较剪枝:只有当一条路径在所有维度上都不劣于另一条时,才丢弃被支配者;只要有一条路径在某个维度更优,两条就同时保留。
比较维度大致包括:
分布类型不同:两者都保留(不同分布各有不可替代的优势)。
是否有 Collector 不同:保留(某些位置——如嵌套循环内层——不能放 Collector)。
分布键的包含关系:更宽的分布键与更精确的分布键各有优势,保留。
hint 命中数:命中多的更优。
强制并行偏好:并行路径优先。
总代价 / 启动代价 / 输出行数:低者优先。
最终选择
枚举完后,从根节点的所有候选中选出最优路径,规则:
1. 命中 hint 的路径优先:若某路径命中的 hint 数最多,则总是选它(hint 覆盖代价比较)。
2. 否则选 总代价最低的路径;若
maximum_slices 开启,则优先选 Collector 最多的路径(最大化 Slice 数)。3. 若选中路径不含 Collector:说明没有任何并行收益,退回串行执行。
最后把选中路径切分为 Leader + Worker Slice 树。顶层 Leader Slice DOP 恒为 1。
与串行优化器的差异
串行优化器(默认传统贪心):枚举连接顺序与访问方法,用单一代价模型选最小代价计划,单线程执行,无并行度 / 分布 / Collector 概念。
维度 | 串行优化器(传统贪心) | 并行查询优化器 |
输入 | 查询块 | 串行优化器产出的计划树 |
输出 | 一棵串行执行计划 | Leader + Worker Slice 树(两层) |
枚举对象 | 连接顺序 + 访问方法 | 分布类型 / DOP / 是否两阶段 |
是否改连接顺序 | 是(贪心选定) | 否,复用串行已选定的计划树 |
代价模型 | 单一串行代价(CPU + IO) | 借用更精细的代价模型,额外叠加 Collector / 通信 / 效率折扣 |
并行度 | 无(恒单线程) | 有(MANUAL 固定 / AUTO 自动) |
分布概念 | 无 | 4种分布类型 + 分布键 + 等价类 |
剪枝 | 按代价 + 有序性等维度 | 按分布类型 / 分布键 / Collector / DOP 源 + 代价多维 |
选择策略 | 选最小代价计划 | hint 优先,其次最小代价,无 Collector 则退回串行 |
触发时机 | 计划生成主路径 | 串行计划之后的并行化后处理 |
可退回串行 | — | 是,多重门控,可能枚举出并行计划却最终退回串行 |
最根本的差异:串行优化器决定"怎么连",并行优化器决定"这棵树怎么并行执行"——两者解耦,连接顺序决策完全在串行阶段完成。
用户控制
这是本文重点。用户通过会话变量和 SQL hint 两类手段控制并行优化器。
启用并行查询
并行查询默认关闭。最简启用:
-- 开启并行,固定 DOP=8(MANUAL 模式)SET SESSION max_parallel_degree = 8;-- 或让优化器自动选 DOPSET SESSION max_parallel_degree = 16; -- AUTO 下的 DOP 上限SET SESSION parallel_degree_policy = AUTO;
说明:
max_parallel_degree = 0 表示关闭并行;> 0 表示开启。AUTO 模式下它作为 DOP 上限。核心会话变量
变量 | 默认 | 作用 |
max_parallel_degree | 0(关闭) | 并行度。MANUAL 下即固定 DOP;AUTO 下为 DOP 上限 |
parallel_degree_policy | MANUAL | DOP 策略: MANUAL(固定)/ AUTO(自动) |
parallel_plan_cost_threshold | 50000 | 全局门控:串行代价 ≥ 此值才考虑并行 |
parallel_scan_cost_threshold | 10000 | MANUAL:表串行代价低于此值不走并行扫描;AUTO:每 Worker 扫描代价达标线 |
parallel_scan_records_threshold | 10000 | 表预估访问行数 < 此值不走并行扫描 |
parallel_scan_ranges_threshold | 2 | 预估范围数 < 此值不走并行扫描 |
parallel_plan_efficiency_rows_scale | 100 | 并行效率折算的行数缩放因子;0禁用效率模型 |
parallel_plan_compare_serial_cost | false | 为真时,允许把"不并行(原串行计划)"纳入候选与并行计划比代价 |
parallel_query_switch | 默认子集 | 位掩码总开关 |
tdsql_enable_parallel_hash_join | false | 开启并行 Hash Join |
parallel_query_switch 位掩码
parallel_query_switch 是一组开关的位掩码,用 开关名=on/off 设置:SET SESSION parallel_query_switch = 'force=on,subquery_pushdown=on';
PARALLEL hint
-- 表级:指定该表用 DOP=4SELECT /*+ PARALLEL(t 4) */ COUNT(*) FROM t;-- 表级 + AUTO:该表自动选 DOPSELECT /*+ PARALLEL(t AUTO) */ COUNT(*) FROM t;-- 表级禁用该表并行SELECT /*+ PARALLEL(t 0) */ ... FROM t;-- 全局:整个查询用 DOP=8SELECT /*+ PARALLEL(8) */ ...;-- 指定扫描类型SELECT /*+ PARALLEL(t 4 DYNAMIC_RANGE) */ ... FROM t; -- 动态范围并行扫描SELECT /*+ PARALLEL(t 4 PARTITION) */ ... FROM t; -- 分区并行扫描
要点:
hint 命中的路径会被加权,选优时优先于代价最低的路径。
显式指定 DOP 或 force 时,会跳过行数 / 代价阈值检查——即"用户明确要求并行,就不再因阈值剪枝"。
PARALLEL(t AUTO) 在该表粒度强制 AUTO,覆盖会话变量。PARALLEL(t 0) 显式禁用该表并行。PQ_DISTRIBUTE hint
-- 两阶段聚合:Worker 局部聚合 + Leader 合并SELECT /*+ PQ_DISTRIBUTE(@qb AGGREGATE NONE GATHER) */ ...FROM /*+ QB_NAME(qb) */ ...;-- 一阶段聚合(gather 到 Leader)SELECT /*+ PQ_DISTRIBUTE(@qb AGGREGATE GATHER) */ ...;-- 排序下推(Worker 排序 + Leader 归并)SELECT /*+ PQ_DISTRIBUTE(@qb SORT NONE GATHER) */ ...;
target 取
TABLE / AGGREGATE / SORT / WINDOW,每个 target 的 first/second 类型取 NONE / GATHER / HASH / BROADCAST / UNSPECIFIED。常见组合:形态 | target | first | second |
两阶段聚合 | AGGREGATE | NONE | GATHER |
一阶段聚合 | AGGREGATE | GATHER | UNSPECIFIED |
下推排序 | SORT | NONE | GATHER |
下推一阶段聚合 | AGGREGATE | NONE | UNSPECIFIED |
强制并行与退回串行
两个机制决定"是否真的走并行":
force(parallel_query_switch)或 PARALLEL hint:强制尽量走并行,跳过阈值检查,选优时偏好并行路径。parallel_plan_compare_serial_cost:为真时,把"不并行(原串行计划)"也作为合法候选,与并行计划比代价。为假(默认)时,无 Gather 的纯串行路径不参与竞争。 但即使 force,若最终选中的路径不含 Collector,仍会退回串行——因为没有任何并行收益。即:force 让路径免于阈值剪枝并优先选中带 hint 的并行路径,但不会强行选择一个毫无并行结构的计划。
观察手段
EXPLAIN:可看到并行计划中的 Collector / 并行扫描 / 分区扫描节点,以及各 Slice 的 DOP。若 EXPLAIN 里没有 Collector 等并行节点,说明该查询未走并行。
optimizer_trace:并行优化输出在
PARALLEL_PLAN_OPTIMIZATION 段,可看到候选路径、剪枝原因、Auto-DOP 计算结果与中止原因、最终 result(no_valid_parallel_plan / best_plan_is_serial / 正常选中)。