在 OceanBase 的 SQL 编译流水线中,Parser 负责把 SQL 文本切成一棵 ParseNode 语法树;紧接其后的 Resolver 阶段则把这棵语法树”翻译”成带有 schema/类型/列引用信息的 ObStmt 语义树。src/sql/resolver/ob_resolver.cpp 就是这个语义解析阶段的总入口:所有 DML、DDL、DCL、TCL、命令语句(SHOW/SET/KILL 等)以及 PL 编译预处理都要经过它。
本文件只有 1522 行,体量不大,但它是 100+ 个具体 Resolver 子类的”分发中心”——只有它能告诉你一条 SQL 是怎么被挑中特定 Resolver 的。理解了这个分发机制,再去看 ob_insert_resolver/ob_create_table_resolver 等具体实现就事半功倍。
一、整体架构位置
SQL 编译完整链路可以简化为:
SQL 文本
→ Lex (词法)
→ Parser (语法 → ParseNode)
→ Resolver (语义 → ObStmt) ← 本文主角
→ Rewriter (规则改写)
→ Optimizer (优化 → ObLogicalPlan)
→ Code Generator (生成执行计划)
→ Executor (执行)
ObResolver 在链路中承上启下:上游接收 Parser 的 ParseNode,下游吐出可直接喂给 Rewriter/Optimizer 的 ObStmt。它的内部核心逻辑只有两个:
- 分发:根据
ParseNode.type_走大 switch,路由到ObXxxResolver子类; - 收尾:在分发之后挂统一的”安全 + 优化”钩子(备库只读、外表写禁用、Hint 解析、富格式开关、DDL 远端转发信息注入)。
这种”中心分发 + 周边收尾”的写法,是为了让每个具体 Resolver 子类保持单一职责(只关心自身语法补全),同时保证跨语句的横切关注点不重不漏。
二、关键数据结构与类骨架
文件涉及的几个核心对象:
- ObResolverParams:编译期上下文,相当于”参数包”。包含
allocator_(内存分配器)、schema_checker_(schema 校验器)、session_info_(会话)、expr_factory_(表达式工厂)、query_ctx_(查询上下文)、sql_proxy_(内部 SQL 代理)、is_prepare_protocol_(是否走 prepare 协议)等。每次 resolve 都会拿到最新的 params,因此 DDL 后立刻解析也能看到最新 schema。 - ObStmt:语义层句柄,所有具体语句(
ObSelectStmt/ObInsertStmt/ObCreateTableStmt等)都派生自它。 - ParseNode:Parser 阶段的语法节点,
type_决定类型(T_SELECT/T_INSERT/T_CREATE_TABLE/…),children_是子节点数组。
ObResolver 自身非常简单——只持一个 params_ 引用,连构造析构都是空操作。所有的”重活”都委托给子 Resolver。
三、两个分发模板:通用与 SELECT 专用
ob_resolver.cpp 提供两个 C++ 模板函数,承担实际的”实例化子 Resolver → 调用 → 拿回 ObStmt”工作:
template <typename ResolverType>
int ObResolver::stmt_resolver_func(ObResolverParams ¶ms,
const ParseNode &parse_tree,
ObStmt *&stmt)
{
int ret = OB_SUCCESS;
HEAP_VAR(ResolverType, stmt_resolver, params) {
if (OB_FAIL(stmt_resolver.resolve(parse_tree))) {
LOG_WARN("execute stmt_resolver failed", K(ret), K(parse_tree.type_));
}
stmt = stmt_resolver.get_basic_stmt();
}
return ret;
}
关键点:HEAP_VAR 是 OceanBase 特有的”栈上对象 + 对象池”组合,能避免堆分配并自动析构。在 SQL 这种高频调用热路径上,比起 std::unique_ptr<new Resolver>,它少一次堆分配、少一次 delete 调用、少一次 cache miss。
SELECT 专用版本则多了两个开关:
template <typename SelectResolverType>
int ObResolver::select_stmt_resolver_func(ObResolverParams ¶ms,
const ParseNode &parse_tree,
ObStmt *&stmt)
{
int ret = OB_SUCCESS;
HEAP_VAR(SelectResolverType, stmt_resolver, params) {
stmt_resolver.set_calc_found_rows(true);
stmt_resolver.set_has_top_limit(true);
if (OB_FAIL(stmt_resolver.resolve(parse_tree))) {
LOG_WARN("execute stmt_resolver failed", K(ret), K(parse_tree.type_));
}
stmt = stmt_resolver.get_basic_stmt();
}
return ret;
}
set_calc_found_rows(true):让 Resolver 收集 MySQL 兼容的SQL_CALC_FOUND_ROWS信息;set_has_top_limit(true):标记外层有 LIMIT,优化器可据此剪掉不必要的排序代价。
四、resolve() 主函数:六阶段流水线
整个 resolve 函数按”前置 → 分发 → 后置”组织,可拆解为 6 个阶段:
阶段 ①:参数校验
5 个核心依赖(allocator/schema_checker/session_info/expr_factory/query_ctx)任一为空即失败。这是硬性前置——所有子 Resolver 都隐式依赖这些指针。
阶段 ②:特殊节点预处理
如果 parse_tree.type_ 是 T_SP_PRE_STMTS(PL 编译前节点),先调用 ObPLResolver 把里面的 SQL 抽出 real_parse_tree 与问号占位符计数;如果是 T_SQL_STMT(Parser 的”外层壳”),剥壳取 children_[0]。其他情况直接指向 parse_tree 自身。
阶段 ③:上下文注入
把 parse_tree.type_ 同步到 params_/session_info_/sql_ctx 三处。这样下游 Optimizer/Executor 通过 session 就能知道当前正在跑什么类型的 SQL,不必重复做 switch。
阶段 ④:大分发(核心)
根据 real_parse_tree->type_ 走 100+ 个 case。代码里通过两个宏把分发写在一行:
#define REGISTER_STMT_RESOLVER(name) \
do { \
ret = stmt_resolver_func<Ob##name##Resolver>(params_, \
*real_parse_tree, stmt); \
} while (0)
switch (real_parse_tree->type_) {
case T_CREATE_TENANT: REGISTER_STMT_RESOLVER(CreateTenant); break;
case T_CREATE_TABLE: REGISTER_STMT_RESOLVER(CreateTable); break;
case T_SELECT: REGISTER_SELECT_STMT_RESOLVER(Select); break;
case T_INSERT: REGISTER_STMT_RESOLVER(Insert); break;
case T_UPDATE: REGISTER_STMT_RESOLVER(Update); break;
// ... 100+ case,覆盖所有 DML/DDL/DCL/TCL/SHOW/KILL/PL 命令 ...
default: {
ret = OB_NOT_SUPPORTED;
const char *type_name = get_type_name(parse_tree.type_);
LOG_WARN("Statement not supported now", K(ret), K(type_name));
LOG_USER_ERROR(OB_NOT_SUPPORTED, "statement type");
}
}
几个值得注意的细节:
- 同族合并:
T_SHOW_TABLES/T_SHOW_DATABASES/… 几十种 SHOW 全部走ObShowResolver,靠 Resolver 内部再 switch 细分; - 编译宏守护:TDE 加密相关 (
OB_BUILD_TDE_SECURITY)、Oracle PL (OB_BUILD_ORACLE_PL)、Shared Storage 模式 (OB_BUILD_SHARED_STORAGE) 等 case 只在对应编译开关下出现; - 降级链:例如
T_DROP_FUNC先尝试ObDropFuncResolver,失败且错误码是OB_ERR_FUNCTION_UNKNOWN时再尝试ObDropFunctionResolver,体现 MySQL/Oracle 兼容差异。
阶段 ⑤:6 步收尾校验
switch 走完后,无论哪条分支,都要经过 6 个安全/优化钩子(每步都 OB_SUCC(ret) 短路):
// ⑤.1 外表写禁:仅 INSERT 允许写外表
if (OB_SUCC(ret) && stmt->is_dml_stmt() && !stmt->is_insert_stmt()) {
OZ( (static_cast<ObDMLStmt*>(stmt)->disable_writing_external_table()) );
}
// ⑤.2 物化视图写禁:非内部 session、非 EXPLAIN、非 stmt_id==0 的 DML
if (OB_SUCC(ret) && !params_.session_info_->is_inner()
&& stmt->is_dml_stmt() && !stmt->is_explain_stmt()
&& 0 == stmt->get_stmt_id()) {
OZ( (static_cast<ObDMLStmt*>(stmt)->disable_writing_materialized_view()) );
}
// ⑤.3 备库只读:写语句落在非 PRIMARY 角色租户上即 OB_STANDBY_READ_ONLY
if (OB_SUCC(ret)) {
if (ObStmt::is_write_stmt(stmt->get_stmt_type(), stmt->has_global_variable())
&& !MTL_TENANT_ROLE_CACHE_IS_PRIMARY_OR_INVALID()) {
ret = OB_STANDBY_READ_ONLY;
TRANS_LOG(WARN, "standby tenant support read only", K(ret), K(stmt));
}
}
// ⑤.4 MySQL 模式 enum/set 字面量包装
if (OB_SUCC(ret) && stmt->is_dml_stmt() && !params_.session_info_->is_varparams_sql_prepare()
&& lib::is_mysql_mode()) {
ObDMLStmt *dml_stmt = static_cast<ObDMLStmt*>(stmt);
ObRawExprWrapEnumSet enum_set_wrapper(*params_.expr_factory_, params_.session_info_);
if (OB_FAIL(enum_set_wrapper.wrap_enum_set(*dml_stmt))) {
LOG_WARN("failed to wrap_enum_set", K(ret));
}
}
// ⑤.5 query_hint 初始化(解析 /*+ ... */ Hint)
if (OB_SUCC(ret) && stmt->is_dml_stmt() && !stmt->is_explain_stmt()) {
if (OB_FAIL(params_.query_ctx_->query_hint_.init_query_hint(...))) { ... }
else if (OB_FAIL(params_.query_ctx_->query_hint_.check_and_set_params_from_hint(...))) { ... }
}
// ⑤.6 内表 + select-for-update 标记 + rich_format 强制开关
if (OB_SUCC(ret) && stmt->is_dml_stmt() && !stmt->is_explain_stmt()) {
bool is_contain_inner_table = false;
bool is_contain_select_for_update = false;
ObDMLStmt *dml_stmt = static_cast<ObDMLStmt*>(stmt);
dml_stmt->check_if_contain_inner_table(is_contain_inner_table);
dml_stmt->check_if_contain_select_for_update(is_contain_select_for_update);
params_.query_ctx_->is_contain_inner_table_ = is_contain_inner_table;
params_.query_ctx_->is_contain_select_for_update_ = is_contain_select_for_update;
params_.query_ctx_->has_dml_write_stmt_ = dml_stmt->is_dml_write_stmt();
// rich format hint:用户可通过 Hint 强制开启/关闭列存富格式
ObOptParamHint &opt_hint = params_.query_ctx_->query_hint_.global_hint_.opt_params_;
opt_hint.check_and_get_bool_opt_param(ObOptParamHint::ENABLE_RICH_VECTOR_FORMAT,
has_rich_format_hint, enable_rich_format);
}
阶段 ⑥:DDL 远端转发信息注入
对所有 DDL/DCL 语句,把”执行租户”和”原始 SQL 文本”塞进 ddl_arg,并生成同步 DDL ID。远端 RS 收到 RPC 后会直接拿这份文本转发执行,避免再次解析。
五、设计动机与延伸
几个有意思的设计决策:
- 为什么 switch 集中在一处:横切关注点(外表、物化视图、备库、Hint、rich format、DDL 注入)写一次就够;如果让每个子 Resolver 各自加,100+ 个文件都要改一遍。
- 为什么不在分发前就做收尾:因为收尾依赖 stmt 的具体类型(dml/ddl/select/insert),必须先等分发完知道 stmt 是什么。
- HEAP_VAR 模式:把 Resolver 对象的生命周期绑在 arena 上,是 OceanBase 高频热路径的常见优化,能在百万 QPS 下显著降低延迟。
- 降级链 (DropFunc → DropFunction):用一个错误码 (
OB_ERR_FUNCTION_UNKNOWN) 触发重试,体现了”先猜一个 Resolver,错了再换”的设计哲学,比 union dispatch 更灵活。
六、小结
ob_resolver.cpp 看起来只是一个超大的 switch,但它把”SQL 分类 → 子 Resolver 调用 → 统一安全/优化收尾”这三件事串联成一个清晰可读的流程。理解它之后再去看任何具体 Resolver(比如 ob_select_resolver 或 ob_create_table_resolver),就能清楚”它在整个编译链路中处于哪个位置、上游拿到了什么、下游要吐出什么”。
下一期会沿着 SQL 流水线继续往下走,看 Rewriter 阶段如何对 ObStmt 做规则改写(子查询展开、谓词下推、别名规范化等)。