OceanBase源码解读:SQL解析器入口 ob_sql_parser

SQL 解析器是数据库引擎的”咽喉”——每条 SQL 必须先被拆解成语法树,才能进入后续的改写、优化和执行阶段。本文解读 OceanBase 中 ob_sql_parser.cpp 这个文件,它虽然只有 124 行,却是连接 C++ 上层框架与 bison/flex 生成的 C 解析器的关键桥梁,也是理解整个解析器架构的最佳切入点。

一、解析器整体架构

OceanBase 的 SQL 解析器采用经典的”bison + flex”方案,但在此基础上做了多层封装。从外到内共有五层:

ObParser (ob_parser.cpp)
  └─ ObSQLParser (ob_sql_parser.cpp)    ← 本文解读
       └─ parse_init / parse_sql (sql_parser_base.c)
            └─ bison: yyparse()          → 语法分析,产出 AST
                 └─ flex: yylex()        → 词法分析,逐 token 喂给 bison

ObParser 是 observer 内部使用的主入口,除了调用解析器,还负责 PL 语句检测、多语句切分、charset 处理等。而 ObSQLParser(本文件)则是一层极薄的封装,它的核心价值在于:让 C++ 代码能以面向对象的方式调用纯 C 的 bison/flex 生成代码,同时通过条件编译 SQL_PARSER_COMPILATION 同时服务 observer 和 obproxy 两个产品。

二、核心数据结构

ParseResult —— 解析结果容器

ParseResult 是贯穿整个解析过程的中枢结构,定义在 parse_node.h 中,包含以下关键字段:

typedef struct {
  void *yyscan_info_;            // flex 扫描器句柄
  const char *input_sql_;        // 原始 SQL 文本
  void *malloc_pool_;            // 内存分配器(ObIAllocator)
  ObSQLMode sql_mode_;           // MySQL/Oracle 兼容模式
  ParamList *param_nodes_;       // 参数链表(fast parse 收集的常量)
  ParseNode *result_tree_;       // AST 根节点
  char *no_param_sql_;           // 参数化后的 SQL(常量→?)
  jmp_buf *jmp_buf_;            // 致命错误时的 longjmp 跳板
  char *error_msg_;              // 错误信息
  PLParseInfo pl_parse_info_;    // PL 解析上下文
  ObMinusStatusCtx minus_ctx_;   // 负数处理上下文
  // ... 标志位:is_fp_/is_multi_query_/need_parameterize_ 等
} ParseResult;

ParseNode —— AST 节点

每个语法元素最终都变成一个 ParseNode,它用 union 兼容终端节点(常量值)和非终端节点(有子节点):

typedef struct _ParseNode {
  ObItemType type_;              // token 类型(T_SELECT/T_WHERE/T_INT 等)
  int32_t num_child_;           // 子节点数量
  union {
    int64_t value_;             // 数值常量值
    int32_t int32_values_[2];
  };
  const char *str_value_;       // 字符串值(非 null 结尾!)
  int64_t str_len_;             // 字符串长度
  struct _ParseNode **children_; // 子节点数组
  ObStmtLoc stmt_loc_;          // 源码位置(行列号)
  // flag_ 位域:is_neg_/is_hidden_const_/is_tree_not_param_ 等 15+ 标志
} ParseNode;

一个值得注意的设计是 str_value_ 不以 结尾,必须配合 str_len_ 使用。这是为了支持同一段文本中多个 token 零拷贝共享内存——bison 只在 malloc_pool_ 中分配一次大块内存,各 ParseNode 的 str_value_ 直接指向其中的偏移。

三、三大核心函数

1. parse() —— 语法分析主入口

int ObSQLParser::parse(const char *str_ptr, const int64_t str_len,
                         ParseResult &result)
{
  int ret = OB_SUCCESS;
#ifndef SQL_PARSER_COMPILATION
  GET_DIAGNOSTIC_INFO->get_ash_stat().in_parse_ = true;
#endif
  if (OB_FAIL(parse_init(&result))) {
    // do nothing
  } else if (OB_FAIL(parse_sql(&result, str_ptr,
                                static_cast(str_len)))) {
    // do nothing
  }
#ifndef SQL_PARSER_COMPILATION
  GET_DIAGNOSTIC_INFO->get_ash_stat().in_parse_ = false;
#endif
  return ret;
}

这个函数的逻辑极其简洁——两步串联:

  1. parse_init:初始化 flex 扫描器。根据 sql_mode_ 选择 MySQL 还是 Oracle 解析器。Oracle 模式下还会根据连接字符集进一步选择变体(single_byte / utf8 / gbk / hkscs),每种字符集对应一套独立生成的 bison/flex 代码。
  2. parse_sql:设置输入缓冲区并调用 yyparse() 执行 LALR(1) 语法分析。成功后 result.result_tree_ 指向 AST 根节点。

在 observer 编译时(非 SQL_PARSER_COMPILATION),额外设置 ASH(Active Session History)的 in_parse_ 标志位,让性能采样系统能精确统计”花在语法解析上的时间”。obproxy 编译时这段代码被条件编译剥离——因为 proxy 环境不含 observer 的诊断框架。

2. parse_and_gen_sqlid() —— 快速解析 + SQL ID 生成

这个函数专供 obproxy 使用(头文件注释明确标注 "only for obproxy fast parser"),observer 内部走的是另一条链路。它的核心目标是:快速生成 sql_id,让 proxy 能基于 SQL 指纹做路由决策。

int ObSQLParser::parse_and_gen_sqlid(void *malloc_pool,
    const char *str_ptr, const int64_t str_len,
    const int64_t len, char *sql_id)
{
  // 1. 分配并初始化 ParseResult
  ParseResult *parse_result = (ParseResult *)parse_malloc(
      sizeof(ParseResult), malloc_pool);
  // 2. 设置 fast parse 标志
  parse_result->is_fp_ = true;            // 启用 fast parse
  parse_result->need_parameterize_ = true; // 常量参数化
  // 3. 分配参数化 SQL 缓冲区
  parse_result->no_param_sql_ = buf;
  // 4. 执行解析
  ret = parse(str_ptr, new_length, *parse_result);
  // 5. 对参数化 SQL 做 MD5 生成 sql_id
  if (OB_SUCC(ret)) {
    ret = gen_sqlid(parse_result->no_param_sql_,
        parse_result->no_param_sql_len_, len, sql_id);
  }
  return ret;
}

Fast parse 是 OceanBase 解析器的一种轻量模式:它不构建完整的 AST,只做词法级别的参数化——把 SQL 中的常量替换为 ?,保留 hint。例如 SELECT * FROM t WHERE id = 42 会被参数化为 SELECT * FROM t WHERE id = ?。这种模式比完整语法分析快得多,非常适合 obproxy 这种需要微秒级响应的组件。

3. gen_sqlid() —— MD5 指纹生成

int ObSQLParser::gen_sqlid(const char* paramed_sql,
    const int64_t sql_len, const int64_t len, char *sql_id)
{
  const int32_t MD5_LENGTH = 16;
  unsigned char md5_buf[MD5_LENGTH];
  unsigned char *res = MD5(
      reinterpret_cast(paramed_sql),
      sql_len, md5_buf);
  ret = parser_to_hex_cstr(md5_buf, MD5_LENGTH, sql_id, len);
  return ret;
}

逻辑很直接:对参数化后的 SQL 文本取 MD5(16 字节),再转成 32 字符的十六进制字符串。这 32 字符的 sql_id 就是 SQL 的”指纹”——SELECT * FROM t WHERE id=1SELECT * FROM t WHERE id=2 会得到相同的 sql_id,因为参数化后都是 SELECT * FROM t WHERE id=?

sql_id 在 OceanBase 中有两个核心用途:

  • Plan Cache 匹配:相同 sql_id → 复用已有执行计划,跳过优化器
  • SQL 审计:作为 SQL 语句的归一化标识,用于审计日志和性能统计

四、解析器生成机制

理解 ob_sql_parser.cpp 的前提是了解底层的生成机制。OceanBase 使用 gen_parser.sh 脚本从 bison .y 文件和 flex .l 文件生成 C 代码:

# MySQL 模式解析器
bison sql_parser_mysql_mode.y → sql_parser_mysql_mode_tab.c
flex sql_parser_mysql_mode.l  → sql_parser_mysql_mode_lex.c

# Oracle 模式(4 种字符集变体)
bison sql_parser_oracle_mode.y → tab.c  (分别处理 single_byte/utf8/gbk/hkscs)

# 全文检索布尔模式解析器
bison ftsparser.y → ftsparser_tab.c

sql_parser_base.c 中的 parse_init() 根据当前 sql_mode_ 和连接字符集,选择对应变体的 yylex_init_extra 函数来初始化扫描器:

int parse_init(ParseResult *p) {
  if (IS_ORACLE_COMPATIBLE) {
    // Oracle 模式:按 collation 选字符集变体
    switch(type) {
      case CHARSET_PARSER_TYPE_UTF8MB4:
        ret = obsql_oracle_utf8_yylex_init_extra(p, &p->yyscan_info_);
        break;
      case CHARSET_PARSER_TYPE_GB:
        ret = obsql_oracle_gbk_yylex_init_extra(p, &p->yyscan_info_);
        break;
      // ... single_byte, hkscs
    }
  } else {
    // MySQL 模式:只有一套
    ret = obsql_mysql_yylex_init_extra(p, &p->yyscan_info_);
  }
}

五、小结

ob_sql_parser.cpp 虽然体量小,但它是理解 OceanBase 解析子系统的”地图”:

  • 分层设计:C++ 薄封装 → C 层分发器 → bison/flex 生成代码,每层职责清晰
  • 双产品复用:通过 SQL_PARSER_COMPILATION 条件编译,同一份代码服务 observer(含 ASH 诊断)和 obproxy(不含)
  • Fast parse 机制:obproxy 通过 fast parse 快速参数化 + MD5 生成 sql_id,避免完整语法分析的开销
  • 多字符集支持:Oracle 模式按连接字符集生成 4 套独立解析器,确保多字节字符(全角空格、全角逗号等)正确分词

从整个 SQL 编译流水线看,解析器产出的 ParseNode 语法树将作为输入传给 resolver(语义分析)、transformer(改写)、optimizer(优化器),最终生成可执行的物理计划。后续文章将继续沿这条流水线深入。

发表回复

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