OceanBase源码解读:SQL语义解析总入口 ob_resolver

在 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 &params,
                                   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 &params,
                                          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_resolverob_create_table_resolver),就能清楚”它在整个编译链路中处于哪个位置、上游拿到了什么、下游要吐出什么”。

下一期会沿着 SQL 流水线继续往下走,看 Rewriter 阶段如何对 ObStmt 做规则改写(子查询展开、谓词下推、别名规范化等)。

发表回复

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