OceanBase源码解读:SQL改写阶段总入口 ob_transformer_impl
一条 SQL 从客户端进入 Observer 进程后,会先后经历「解析 → 改写 → 优化 → 代码生成」四个阶段。其中改写阶段(Rewrite)的核心目标是:在不改变 SQL 语义的前提下,把语句对象(ObDMLStmt)”重塑”成更易于优化器(CBO)处理的形态。本文聚焦 1247 行的 ob_transformer_impl.cpp——它把 30 余种改写规则(视图合并、子查询去关联、OR 展开、谓词下推、count 转 exists 等)收敛到一个统一的调度框架内,是改写阶段的总指挥。
一、架构位置与上下游关系
ObTransformerImpl 运行在 Observer 进程的 SQL 引擎线程上,其位置可以概括为:
- 上游:
sql/resolver产出的ObDMLStmt语义树 +QueryCtx(包含 session、hint、参数化状态) - 下游:
sql/optimizer接收”已被等价改写、表达更顺”的 Stmt 形式,再做连接顺序、访问路径等成本决策 - 同级:30 余种
ObTransformRule子类(ObTransformViewMerge、ObTransformOrExpansion、ObTransformDecorrelate等),每个规则都是一个独立的ob_transform_*.cpp实现
改写阶段的工程动机是:让 CBO 拿到的不是”用户写出来的原始 SQL 形式”,而是经过等价变换、规则驱动优化后的”更顺手的输入”。
二、核心数据结构
int ObTransformerImpl::transform(ObDMLStmt *&stmt)
{
int ret = OB_SUCCESS;
bool trans_happended = false;
if (OB_ISNULL(stmt)) {
ret = OB_ERR_UNEXPECTED;
LOG_WARN("get unexpected null", K(ret));
} else if (OB_FAIL(set_transformation_parameters(stmt->get_query_ctx()))) {
LOG_WARN("failed to extract trans ctx param", K(ret));
} else if (OB_FAIL(do_transform_dblink_write(stmt, trans_happended))) {
...
} else if (trans_happended) {
//dml write query will be executed in remote, do not need transform
} else if (OB_FAIL(SMART_CALL(get_stmt_trans_info(stmt, true)))) {
...
} else if (OB_FAIL(do_transform(stmt))) {
LOG_WARN("failed to do transform", K(ret));
} else if (OB_FAIL(do_after_transform(stmt, ctx_->session_info_))) {
...
} else {
print_trans_stat();
}
return ret;
}
几个关键数据结构:
ObDMLStmt *&stmt:被改写的语句对象,in-out引用。改写可能把它替换为更复杂的 Stmt 树(例如把视图展开成 join)。needed_transform_types_:64 位位图,每种规则占 1 bit。决定本次会话”允许运行”的规则子集。happened_cost_based_trans_:记录”已被 CBO 触发”的成本型改写,用于跨规则协作避免重复。trans_count_[i]:每种规则的触发次数统计,仅用于print_trans_stat()输出。max_iteration_count_:单条 query 的最大改写迭代次数,防止规则互改导致死循环。
三、改写流水线的关键流程
1. transform:总入口的”瀑布式”错误处理
transform() 用了一种 OceanBase 标志性的写法——”错误码瀑布”:连续 if (OB_FAIL(...)) 逐层下推,任一步失败立即 LOG_WARN 后返回,把回滚留给上层。这是 OceanBase 服务端代码的常见模式,因为上层会基于 ret 值做”逆序回滚”和资源释放。
完整步骤骨架:
set_transformation_parameters:从 QueryCtx 拉出 hint、参数化程度等开关do_transform_dblink_write:若为 dblink 写入,直接退出,让远端执行get_stmt_trans_info:扫描 stmt 内函数特征,识别StmtFunc(全文检索/枚举/链接表等)formalize_implicit_distinct:补齐 union 等隐式 distinct 语义do_prepare_mv_rewrite:物化视图改写准备do_transform:核心,跑启发式 + 成本改写do_transform_dblink_read:dblink 读取的语句包装do_after_transform:参数 final 类型校验收尾
2. transform_rule_set:迭代求不动点
int ObTransformerImpl::transform_rule_set(ObDMLStmt *&stmt,
uint64_t needed_types,
int64_t iteration_count,
bool& trans_happened)
{
int ret = OB_SUCCESS;
trans_happened = false;
if (0 != (needed_types & needed_transform_types_)) {
bool need_next_iteration = true;
int64_t i = 0;
for (i = 0; OB_SUCC(ret) && need_next_iteration && i < iteration_count; ++i) {
bool trans_happened_in_iteration = false;
ctx_->iteration_level_ = i;
...
if (OB_FAIL(transform_rule_set_in_one_iteration(stmt,
needed_types,
trans_happened_in_iteration))) {
...
} else if (!trans_happened_in_iteration) {
need_next_iteration = false;
} else if (OB_FAIL(stmt->formalize_query_ref_exprs())) {
...
} else if (OB_FAIL(stmt->formalize_stmt(ctx_->session_info_, false))) {
...
} else {
need_next_iteration = true;
trans_happened = true;
}
}
if (OB_SUCC(ret) && need_next_iteration && i == max_iteration_count_) {
ret = OB_E(EventTable::EN_CHECK_REWRITE_ITER_CONVERGE) OB_SUCCESS;
...
}
}
return ret;
}
这是改写循环的灵魂。设计要点是:
- 白名单求交:
needed_types & needed_transform_types_ == 0时直接返回,避免无效调用 - 不动点求解:规则之间会互相影响(A 改完让 B 又能触发),必须迭代到不再变化为止;一旦本轮没有任何规则触发,
need_next_iteration = false提前结束 - 每次循环 formalize:改写改变了 stmt 内部引用关系,必须重算 query_ref_exprs/stmt_expr_reference,否则下游优化器会拿到不一致的状态
- 防死循环:达到
max_iteration_count_时由EN_CHECK_REWRITE_ITER_CONVERGE控制是否报错
3. transform_rule_set_in_one_iteration:30 种规则的执行序
单次迭代内通过宏 APPLY_RULE_IF_NEEDED 顺序应用各种规则。宏会自动判断 (needed_types & rule_type) == 0 来跳过来关闭的规则。顺序设计原则:
- 先做 SIMPLIFY_*_EXPR 类:先常量折叠、死代码消除,否则后续规则面对的是”复杂但没必要的”表达式
- 再做 VIEW_MERGE / COUNT_TO_EXISTS:把视图子查询展开为 join,让 CBO 看见连接结构
- 然后 WHERE_SQ_PULL_UP / DECORRELATE:把 WHERE 中的相关子查询去关联,半连接转内连接
- 接着 JOIN_ELIMINATION / LEFT_JOIN_TO_ANTI:消除空连接、改写反连接
- 最后 PREDICATE_MOVE_AROUND / OR_EXPANSION:谓词下推、可能增加行数但能命中索引的展开
源码顶部那段大注释明确写着:”The ordering to apply the following rules is important, think carefully when new rules are added”——加新规则时必须评估顺序影响。
4. choose_rewrite_rules:动态规则白名单
这是改写阶段最”业务化”的逻辑:根据 stmt 的特征动态决定”本次改写启用哪些规则”。算法分四步:
check_stmt_functions+check_temp_table_functions扫描出StmtFunc(几十个 bool 特征)- 按特征累加到
disable_list:几何/向量近似索引直接全禁;全文检索禁用 WHERE_SQ_PULL_UP 等 11 种会改写文本检索结构的规则 - 某些场景(如 unpivot、enum_set、dblink 链表)用”反向”方式——先开 enable 列表再
~取反 - 最终
need_types = ALL_TRANSFORM_RULES & (~disable_list)
典型例子:包含 dblink 链表的 SQL 会被限制在”不会生成隐式 cast 常量”的子规则内,原因是改写后的 SQL 必须能序列化回文本形式发到远端执行。源码里那个 create table t (c1 varchar(10), c2 char(10)) ... c2 = implicit cast('a' as varchar) 的注释,就是这条限制的直接证据。
5. finalize_exec_params:参数下标分配
改写过程会引入”两阶段参数”(prepare 阶段占位 + execute 阶段具体值)。改写完成后必须为每个 ObExecParamRawExpr 分配一个不冲突的 param_index,让后续物理 plan 的 ?N 占位符和这个下标对齐。遍历范围包括:当前 stmt 的所有 TableItem.exec_params_、所有 ObQueryRefRawExpr.get_exec_params(),以及通过 SMART_CALL 递归的 child_stmts。
6. verify_all_expr_types:类型完整性自检
改写规则若写错(cast 失去精度、子查询列类型推错),会让 CBO 拿到错误的 result type。本函数通过对比”改写前 / 改写后”的 ObRawExprResType 数组检测这类问题,由 trace point EN_CHECK_EXPR_FORMALIZE 控制是否启用。生产环境通常关闭以省开销,测试环境会硬性返回错误。
四、设计动机总结
读完 ob_transformer_impl.cpp 1247 行,可以提炼出几条核心设计选择:
- 规则抽象到子类:每条改写规则单独一个
ob_transform_xxx.cpp,主文件只负责调度,避免一个 4000+ 行的巨型文件 - 不动点迭代:用
transform_rule_set这种”循环直到不再变化”的模式处理规则互改,配套max_iteration_count_防死循环 - 错误码瀑布:连续
OB_FAIL链 + 上层”逆序回滚”,是 OB 服务端代码的标志性写法 - 声明式 + 特征驱动:用
StmtFunc几十个 bool 特征来动态裁剪规则集,而不是写一堆 if-else - 类型自检:
verify_all_expr_types这种”前后快照比对”的稳健性保护
改写阶段的本质不是让 SQL 跑得更快,而是让优化器拿到一个”等价但更顺手”的输入。明白了这一点,再读后续的 optimizer 和 code_generator 就有了共同语境。