OceanBase源码解读:SQL总入口ob_sql.cpp

如果把 OceanBase 的 SQL 引擎比作一条流水线,src/sql/ob_sql.cpp 就是流水线的大门。所有外部 SQL——无论是普通文本协议、Prepared Statement,还是 PL 块里的内嵌 SQL——最终都会先汇聚到 ObSql 这个类,再被分发到解析器、解析器、改写器、优化器、代码生成器,最后拿到可执行的物理计划。本文聚焦这一”总入口”,把它的职责、核心数据结构、以及一条 SQL 从进门到出门的完整流程讲清楚。

一、ObSql 在整体架构中的位置

observer 进程启动时,ObServer 会调用 ObSql::init() 完成初始化:注入统计管理器、RPC 传输层、虚拟表扫描服务、本机地址以及 RS 管理器。初始化成功后,ObSql 内部的工作队列开始运行,准备接收 SQL。

此后,客户端发来的每一条 SQL 都会根据协议类型进入不同的入口:

  • 普通文本协议ObSql::stmt_query()
  • Prepared Statement 的 PREPAREObSql::stmt_prepare()
  • Prepared Statement 的 EXECUTEObSql::stmt_execute()

这三个入口最终都会落入同一条编译流水线:解析 → 改写 → 优化 → 代码生成,区别只在于参数化方式和计划缓存的 KEY 策略。这样设计的好处非常明显:文本协议和 PS/PL 协议共享同一套优化与代码生成逻辑,维护成本和优化收益都集中在 ob_sql.cpp 内部。

二、核心数据结构

理解 ob_sql.cpp 之前,必须先认识四类贯穿全流程的数据结构:

1. ObSqlCtx —— 一次 SQL 执行的上下文

ObSqlCtx 是”命令级”的上下文,从入口一直传递到编译流水线的最末端。它携带的关键信息包括:

  • session_info_:当前会话,保存字符集、租户、事务状态等。
  • schema_guard_:当前时刻的 schema 快照,用于解析和优化期间读取表/索引/列定义。
  • sql_id_ / cur_sql_ / raw_sql_:SQL 文本及其标识。
  • spm_ctx_:SQL Plan Baseline 相关上下文,控制 plan outline 与基线。
  • multi_stmt_item_is_prepare_protocol_is_dynamic_sql_ 等:标记批量语句、PS 协议、动态 SQL 等特殊形态。

2. ObResultSet —— 结果集与执行上下文容器

它不仅是”结果集”,也是执行期间的资源容器。内部持有 ObExecContext、字段列 ObField、物理计划指针、以及 ObCacheObjGuard 等。编译阶段生成的 ObPhysicalPlan 就挂在这里,执行阶段再被引擎消费。

3. ObPlanCacheCtx —— 计划缓存上下文

计划缓存是 OceanBase 性能的关键。ObPlanCacheCtx 封装了:

  • fp_result_.pc_key_:计划缓存的 KEY,可能是 SQL 文本、PS key_id,或参数化后的模板 SQL。
  • fp_result_.parameterized_params_:被参数化的常量列表。
  • ab_params_:数组绑定时的批量参数。
  • should_add_plan_self_add_plan_:控制生成后是否入缓存。

4. ParseResult / ObStmt / ObLogPlan / ObPhysicalPlan —— 四层产物

  • ParseResult:词法语法分析后的抽象语法树(AST)。
  • ObStmt:逻辑语句对象,如 ObSelectStmtObInsertStmt,已经绑定 schema 和语义。
  • ObLogPlan:代价优化器生成的逻辑计划,包含算子树和统计信息。
  • ObPhysicalPlan:代码生成后的物理计划,可直接交给执行引擎。

三、关键流程:一条文本 SQL 如何走完编译流水线

以普通文本协议为例,主链路是 stmt_query → handle_text_query → generate_physical_plan → generate_plan → pc_get_plan / pc_add_plan。下面按阶段拆解。

1. 入口:stmt_query

int ObSql::stmt_query(const common::ObString &stmt,
                      ObSqlCtx &context,
                      ObResultSet &result)
{
  LinkExecCtxGuard link_guard(result.get_session(), result.get_exec_context());
  FLTSpanGuard(sql_compile);
  ObTruncatedString trunc_stmt(stmt);
  // ...
  if (OB_FAIL(sanity_check(context))) {
    LOG_WARN("Failed to do sanity check", K(ret));
  } else if (OB_FAIL(handle_text_query(stmt, context, result))) {
    // ...
  }
  CHECK_STMT_SUPPORTED_BY_TXN_FREE_ROUTE(result, true);
  // ...
}

这里做了三件关键的事:

  • LinkExecCtxGuard:把会话与执行上下文关联起来,保证生命周期安全。
  • FLTSpanGuard(sql_compile):开启全链路追踪的 sql_compile 跨度,用于性能诊断。
  • ObTruncatedString:对超长 SQL 做截断,避免日志和 Trace 爆量。

sanity_check 通过后,真正的处理交给 handle_text_query

2. 总调度:generate_physical_plan

对 DML 类语句,generate_physical_plan 是编译流水线的总调度函数:

if (OB_FAIL(generate_stmt(parse_result, pc_ctx, sql_ctx, allocator, result,
                          basic_stmt, outline_parse_result, outline_state))) {
  LOG_WARN("Failed to generate stmt", K(ret));
} else if (OB_FAIL(ObPrivilegeCheck::check_privilege_new(sql_ctx, basic_stmt, ...))) {
  LOG_WARN("Failed to check ora privilege info", K(ret));
} else if (basic_stmt->is_dml_stmt() || basic_stmt->is_explain_stmt() || basic_stmt->is_help_stmt()) {
  if (OB_FAIL(generate_plan(parse_result, pc_ctx, sql_ctx, result, mode, basic_stmt, ...))) {
    LOG_WARN("failed to generate plan", K(ret));
  }
}

它的职责可以概括为:

  1. 调用 generate_stmt 把 AST 转成逻辑语句 ObStmt
  2. 做权限检查、密码过期检查。
  3. 对 DML/EXPLAIN/HELP 语句调用 generate_plan 生成物理计划;其他命令直接转 ObICmd 并填充结果集。
  4. 编译结束后,对 DML 语句维护对象依赖表,确保后续 DDL 能让相关计划失效。

3. 解析与语义分析:generate_stmt

generate_stmt 主要做三件事:构造 ObSchemaChecker、初始化 ObResolverParams、调用 ObResolver::resolve()

resolver_ctx.allocator_  = &allocator;
resolver_ctx.schema_checker_ = schema_checker;
resolver_ctx.session_info_ = context.session_info_;
resolver_ctx.expr_factory_ = result.get_exec_context().get_expr_factory();
resolver_ctx.stmt_factory_ = result.get_exec_context().get_stmt_factory();
resolver_ctx.cur_sql_ = context.cur_sql_;
// ...
ObResolver resolver(resolver_ctx);
ret = resolver.resolve(ObResolver::IS_NOT_PREPARED_STMT,
                       *parse_result.result_tree_->children_[0], stmt);

resolver 会把 parser 产出的 AST 节点,结合 schema 和 session 信息,转换为高层语义对象。同时它还会产出大量”约束”:

  • 常量参数约束
  • 等价参数约束
  • 表达式约束
  • 权限约束

这些约束会被保存到 ObSqlCtx 中,供计划缓存匹配时使用——这是 OceanBase 计划缓存能安全复用计划的核心依据。

4. DML 四段流水线:generate_plan

这是 SQL 引擎最重的函数之一,标准顺序如下:

// 1. 构造优化器上下文
ObOptimizerContext optctx(...);
// 2. 分配物理计划壳子
ObCacheObjectFactory::alloc(guard, ObLibCacheNameSpace::NS_CRSR, effective_tid);
phy_plan = static_cast(guard.get_cache_obj());
// 3. 规则改写
transform_stmt(...);
// 4. 参数化后重建 SQL
generate_stmt_with_reconstruct_sql(...);
// 5. 代价优化
optimize_stmt(optimizer, *session_info, *stmt, logical_plan);
// 6. 表达式约束
create_expr_constraints(*stmt->get_query_ctx(), result.get_exec_context());
// 7. 代码生成
code_generate(sql_ctx, result, stmt, ..., logical_plan, phy_plan);
// 8. outline / SPM 绑定
prepare_outline_for_phy_plan(logical_plan, phy_plan);

4.1 transform_stmt:规则改写

transform_stmt 是 RBO 层入口。它构造 ObTransformerCtx,然后调用 ObTransformerImpl::transform() 完成子查询提升、视图合并、OR-Expansion 等改写。如果发生 OR-Expansion,还会设置 physical_plan_ctx->set_or_expand_transformed(true),供后续约束匹配使用。

4.2 generate_stmt_with_reconstruct_sql:重建参数化 SQL

为了计划缓存能按”模板”查找 SQL,OceanBase 会把原始 SQL 中的常量参数替换成 ?,生成一份参数化 SQL。这一步在 PS 模式下尤为重要——PS 协议的 no_param_sql 就是模板。

4.3 optimize_stmt:代价优化

调用 ObOptimizer::optimize() 生成 ObLogPlan。优化器会基于统计信息、系统负载、session hint 选择最优算子,比如决定用哪个索引、JOIN 顺序、是否走 SORT。

4.4 code_generate:逻辑计划 → 物理计划

ObCodeGeneratorObLogPlan 编译成 ObPhysicalPlan。这里会结合最小集群版本、是否启用 JIT、是否使用 rich format 等因素,生成最终可执行的算子树。

5. 计划缓存:pc_get_plan 与 pc_add_plan

计划缓存的命中与加入,分别由这两个函数负责。

5.1 取计划 pc_get_plan

if (OB_FAIL(execute_get_plan(*plan_cache, pc_ctx, guard))) {
  if (OB_EAGAIN == ret || OB_REACH_MAX_CONCURRENT_NUM == ret
      || OB_ERR_PROXY_REROUTE == ret || OB_BATCHED_MULTI_STMT_ROLLBACK == ret
      || OB_NEED_SWITCH_CONSUMER_GROUP == ret) {
    /*do nothing*/
  } else {
    get_plan_err = ret;
    ret = OB_SUCCESS; // 内部错误吞掉,降级重新生成计划
  }
}

设计要点:

  • 命中后做权限二次校验,避免 schema/权限变更后复用旧计划。
  • 需要重路由、并发限流、消费者组切换等语义化错误必须透传。
  • 普通 plan cache 内部错误会被吞掉并降级为重新生成计划,保证 SQL 正确性不被缓存问题影响。

5.2 加计划 pc_add_plan

if (PC_PS_MODE == pc_ctx.mode_ || PC_PL_MODE == pc_ctx.mode_) {
  pc_ctx.fp_result_.pc_key_.key_id_ = pc_ctx.sql_ctx_.statement_id_;
  if (pc_ctx.sql_ctx_.is_remote_sql_) {
    pc_ctx.fp_result_.pc_key_.key_id_ = 0;
    pc_ctx.fp_result_.pc_key_.name_ = pc_ctx.raw_sql_;
  }
  ret = plan_cache->add_ps_plan(phy_plan, pc_ctx);
} else {
  check_template_sql_can_be_prepare(pc_ctx, *phy_plan);
  ret = plan_cache->add_plan(phy_plan, pc_ctx);
}

设计要点:

  • PS/PL 模式用 key_id 作为 KEY;远程 SQL 为了防止与普通文本计划冲突,用 key_id=0 + name=参数化 SQL 组合。
  • 文本模式会额外检查参数化后的模板 SQL 是否能在远端被 prepare,避免后续 RPC 阶段解析失败。
  • 重复计划、内存上限、大小上限、不支持的计划等错误都被吞掉,只影响缓存命中率,不影响执行正确性。

四、Prepared Statement 的特殊路径

PS 协议分为 PREPARE 和 EXECUTE 两个阶段,对应 handle_ps_preparehandle_ps_execute

1. handle_ps_prepare:先编译,再缓存元信息

ObPsCache *ps_cache = session.get_ps_cache();
ps_cache->ref_stmt_item(ps_key, stmt_item); // 按 db_id + SQL 文本查找
if (未命中) {
  need_do_real_prepare = true;
} else if (schema 过期) {
  淘汰旧缓存,need_do_real_prepare = true;
}
if (need_do_real_prepare) {
  do_real_prepare(stmt, context, result, is_inner_sql);
}

PREPARE 阶段会把物理计划生成好,并把 ObPsStmtInfo 注册到会话级 PS Cache。后续 EXECUTE 阶段只需按 client_stmt_id 映射到 inner_stmt_id,再取计划即可。

2. handle_ps_execute:参数绑定与复用

EXECUTE 阶段的核心是:

  1. client_stmt_id 查到 inner_stmt_id
  2. 校验传入参数个数与问号数量一致。
  3. 组合固定参数和动态参数,构造 ParamStore
  4. 对 DML 走 PC_PS_MODE 计划缓存;对匿名块/CALL 重新解析生成计划。

这样设计让 PS 协议在首次编译后,后续执行基本免除完整编译开销,只需做参数替换和计划命中。

五、小结

ob_sql.cpp 是 OceanBase SQL 引擎当之无愧的大门。它用一个统一的 generate_physical_plan 流水线,把文本协议、PS 协议、PL 内嵌 SQL 的编译过程收敛到同一套解析-改写-优化-代码生成路径上。围绕它有几条值得记住的设计原则:

  1. 入口分层:stmt_query / stmt_prepare / stmt_execute 只负责协议适配和错误兜底,真正的编译逻辑下沉到 handle_text_query / handle_ps_prepare / handle_ps_execute
  2. 错误码瀑布:大量 OB_FAIL 宏让代码层层嵌套,这是一种统一的错误处理风格;同时 plan cache 相关错误被有意识地吞掉,避免缓存问题影响 SQL 正确性。
  3. 计划缓存解耦:pc_get_planpc_add_plan 只负责命中与加入,不介入编译流程本身,使得缓存策略可以独立演进。
  4. PS 一次编译多次执行:PREPARE 阶段完成完整编译并缓存元信息,EXECUTE 阶段只做参数绑定和计划复用,这是 OceanBase 实现高吞吐 OLTP 的关键之一。

下一篇我们将进入 sql/parser,看看 SQL 字符串是如何被 parser 切成 AST 的。

发表回复

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