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;
}
这个函数的逻辑极其简洁——两步串联:
- parse_init:初始化 flex 扫描器。根据
sql_mode_选择 MySQL 还是 Oracle 解析器。Oracle 模式下还会根据连接字符集进一步选择变体(single_byte / utf8 / gbk / hkscs),每种字符集对应一套独立生成的 bison/flex 代码。 - 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=1 和 SELECT * 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(优化器),最终生成可执行的物理计划。后续文章将继续沿这条流水线深入。