OceanBase源码解读:SQL改写阶段总入口 ob_transformer_impl

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 子类(ObTransformViewMergeObTransformOrExpansionObTransformDecorrelate 等),每个规则都是一个独立的 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 值做”逆序回滚”和资源释放。

完整步骤骨架:

  1. set_transformation_parameters:从 QueryCtx 拉出 hint、参数化程度等开关
  2. do_transform_dblink_write:若为 dblink 写入,直接退出,让远端执行
  3. get_stmt_trans_info:扫描 stmt 内函数特征,识别 StmtFunc(全文检索/枚举/链接表等)
  4. formalize_implicit_distinct:补齐 union 等隐式 distinct 语义
  5. do_prepare_mv_rewrite:物化视图改写准备
  6. do_transform:核心,跑启发式 + 成本改写
  7. do_transform_dblink_read:dblink 读取的语句包装
  8. 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 来跳过来关闭的规则。顺序设计原则:

  1. 先做 SIMPLIFY_*_EXPR 类:先常量折叠、死代码消除,否则后续规则面对的是”复杂但没必要的”表达式
  2. 再做 VIEW_MERGE / COUNT_TO_EXISTS:把视图子查询展开为 join,让 CBO 看见连接结构
  3. 然后 WHERE_SQ_PULL_UP / DECORRELATE:把 WHERE 中的相关子查询去关联,半连接转内连接
  4. 接着 JOIN_ELIMINATION / LEFT_JOIN_TO_ANTI:消除空连接、改写反连接
  5. 最后 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 的特征动态决定”本次改写启用哪些规则”。算法分四步:

  1. check_stmt_functions + check_temp_table_functions 扫描出 StmtFunc(几十个 bool 特征)
  2. 按特征累加到 disable_list:几何/向量近似索引直接全禁;全文检索禁用 WHERE_SQ_PULL_UP 等 11 种会改写文本检索结构的规则
  3. 某些场景(如 unpivot、enum_set、dblink 链表)用”反向”方式——先开 enable 列表再 ~ 取反
  4. 最终 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 就有了共同语境。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注