如果把 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 的 PREPARE →
ObSql::stmt_prepare() - Prepared Statement 的 EXECUTE →
ObSql::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:逻辑语句对象,如
ObSelectStmt、ObInsertStmt,已经绑定 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));
}
}
它的职责可以概括为:
- 调用
generate_stmt把 AST 转成逻辑语句ObStmt。 - 做权限检查、密码过期检查。
- 对 DML/EXPLAIN/HELP 语句调用
generate_plan生成物理计划;其他命令直接转ObICmd并填充结果集。 - 编译结束后,对 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:逻辑计划 → 物理计划
ObCodeGenerator 把 ObLogPlan 编译成 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_prepare 和 handle_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 阶段的核心是:
- 用
client_stmt_id查到inner_stmt_id。 - 校验传入参数个数与问号数量一致。
- 组合固定参数和动态参数,构造
ParamStore。 - 对 DML 走
PC_PS_MODE计划缓存;对匿名块/CALL 重新解析生成计划。
这样设计让 PS 协议在首次编译后,后续执行基本免除完整编译开销,只需做参数替换和计划命中。
五、小结
ob_sql.cpp 是 OceanBase SQL 引擎当之无愧的大门。它用一个统一的 generate_physical_plan 流水线,把文本协议、PS 协议、PL 内嵌 SQL 的编译过程收敛到同一套解析-改写-优化-代码生成路径上。围绕它有几条值得记住的设计原则:
- 入口分层:
stmt_query / stmt_prepare / stmt_execute只负责协议适配和错误兜底,真正的编译逻辑下沉到handle_text_query / handle_ps_prepare / handle_ps_execute。 - 错误码瀑布:大量
OB_FAIL宏让代码层层嵌套,这是一种统一的错误处理风格;同时 plan cache 相关错误被有意识地吞掉,避免缓存问题影响 SQL 正确性。 - 计划缓存解耦:
pc_get_plan和pc_add_plan只负责命中与加入,不介入编译流程本身,使得缓存策略可以独立演进。 - PS 一次编译多次执行:PREPARE 阶段完成完整编译并缓存元信息,EXECUTE 阶段只做参数绑定和计划复用,这是 OceanBase 实现高吞吐 OLTP 的关键之一。
下一篇我们将进入 sql/parser,看看 SQL 字符串是如何被 parser 切成 AST 的。