尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

MySQL SQL解析实战:yacc与Lex如何将SQL语句转换为语法树

MySQL SQL解析实战:yacc与Lex如何将SQL语句转换为语法树 1. 项目概述从“Hello World”到数据库引擎如果你写过C语言第一个程序多半是printf(Hello World);。编译器默默地把这几行字符变成了机器能懂的指令。但如果你要自己设计一门语言或者像MySQL那样需要理解用户输入的SELECT * FROM users WHERE id 1;这样的字符串并把它变成一系列可执行的操作你该怎么办这就是编译原理领域经典组合——yacc和Lex——大显身手的地方。今天我们不谈高深的编译理论就从一个数据库开发者的实战视角聊聊在MySQL源码中yacc和Lex这对“黄金搭档”是如何扮演“SQL翻译官”的角色的。简单来说Lex或Flex负责“认单词”yacc或Bison负责“懂语法”。当MySQL服务器接收到客户端发来的一串SQL语句时它首先面对的就是一堆毫无结构的字符流。Lex的工作就是像扫描仪一样把这些字符流切割成一个个有意义的“单词”Token比如把SELECT识别为“查询关键字”*识别为“通配符”FROM识别为“子句开始”。接着yacc登场它根据预先定义好的SQL语法规则检查这些Token的排列顺序是否合法并最终构建出一棵“语法树”Parse Tree。这棵树就是MySQL理解用户意图的内部蓝图后续的查询优化、执行引擎都基于此展开。所以研究MySQL源码中的yacc和Lex绝不是纸上谈兵。它能让你真正洞悉一条SQL语句从字符串到执行计划的完整解析链路无论是为了深度优化、定制SQL方言还是排查诡异的语法解析错误这部分知识都是绕不开的核心。接下来我们就深入MySQL 8.0的源码腹地拆解这套解析引擎的构造与运作。2. 核心组件解析Lex与yacc的分工与协作理解它们最好从它们各自生成的代码文件开始。在MySQL源码目录如sql/下你会找到几个关键文件sql_yacc.yyyacc的语法规则文件、lex.h和sql_lex.hToken定义、以及由构建系统自动生成的sql_yacc.cc和sql_yacc.h等。它们的协作关系构成了一个清晰的管道。2.1 LexSQL词法分析器的实现细节Lex的核心是一个名为MYSQLlex的函数通常在生成的lex.yy.cc或相关文件中。它的本质是一个有限状态自动机。我们来看一个简化版的内部逻辑// 伪代码示意Lex如何工作 int MYSQLlex(YYSTYPE *yylval, YYLTYPE *yylloc, THD *thd) { char *yytext yy_lexer_buffer; // 当前扫描的字符指针 while (*yytext ! \0) { switch (guess_token_type(yytext)) { case WHITESPACE: skip_spaces(yytext); // 跳过空格 break; case ALPHA: // 遇到字母可能是关键字或标识符 char *word_start yytext; while (is_alnum(*yytext)) yytext; size_t word_len yytext - word_start; char *word strndup(word_start, word_len); // 关键步骤查哈希表判断是否为关键字 int kw_token check_keyword_hash(word, word_len); if (kw_token) { return kw_token; // 返回关键字Token如 SELECT_TOKEN } else { yylval-lexer.string word; // 存入标识符名 return IDENTIFIER; // 返回标识符Token } break; case NUMBER: // 解析数字如 123, 3.14 yylval-number parse_number(yytext); return NUMERIC_LITERAL; case QUOTE: // 解析字符串字面量如 hello yylval-string parse_quoted_string(yytext); return TEXT_STRING; case SYMBOL: // 解析符号如 *, , (, ) switch (*yytext) { case *: yytext; return STAR; case : yytext; return EQUAL_OPERATOR; // ... 其他符号 } break; } } return 0; // EOF }几个实操要点与避坑经验关键字哈希表MySQL维护了一个巨大的关键字哈希表sql/lex.h中定义。check_keyword_hash函数是性能关键。它通过预计算的哈希值快速判断一个单词是否是SQL关键字如SELECT、WHERE还是用户自定义的表名或列名标识符。这里有个坑MySQL在不同SQL模式下关键字集合可能不同。例如BEGIN在存储过程里是关键字但在标准语句中可能不是。Lex需要根据当前会话的sql_mode来动态调整这个判断逻辑。上下文敏感的词法分析纯粹的Lex是上下文无关的但SQL解析有时需要上下文。经典例子是.点号。在SELECT a.b FROM t中.是用于限定列名的运算符但在SELECT 3.14中.是浮点数的一部分。MySQL的Lex通过一个简单的状态机lexer-context来处理当预期一个数字时点号被吸收为数字的一部分在其他上下文中则被识别为单独的DOT_OPERATORToken。这需要在Lex规则中嵌入少量状态判断代码。字符集与转义处理这是字符串和标识符解析中最容易出错的地方。Lex必须正确识别字符集如_utf8mb4 中文并处理转义字符如\、\n。MySQL的实现在sql_lex.cc的scan_ident_sysvar、scan_string等函数里。一个常见问题如果转义处理不当会导致字符串截断错误甚至SQL注入漏洞在解析层面就被埋下隐患。2.2 yaccSQL语法规则的骨架定义yacc的输入文件sql_yacc.yy定义了SQL语言的文法。它采用巴科斯范式BNF或其扩展形式。文件结构大致如下%{ // C代码序言包含头文件和全局声明 #include sql_class.h %} // Token声明与Lex输出的Token对应 %token SELECT INSERT UPDATE DELETE %token IDENTIFIER NUMERIC_LITERAL TEXT_STRING %token EQUAL_OPERATOR STAR DOT // 优先级和结合性声明用于消除歧义 %left %left - %left * / %% // 语法规则部分 query: select_statement | insert_statement | update_statement ; select_statement: SELECT select_list FROM table_references [WHERE where_condition] [ORDER BY order_list] [LIMIT limit_options] ; select_list: STAR | select_expr_list ; select_expr_list: select_expr | select_expr_list , select_expr ; select_expr: expression [AS IDENTIFIER] ; // ... 更多规则 %% // 辅助C函数如错误处理规则解析与设计思想递归定义注意select_expr_list的定义它使用了左递归A: A , B。yacc能高效处理这种递归从而解析任意长度的表达式列表。这是定义列表结构如字段列表、参数列表的标准手法。动作代码Action每个语法规则后面可以跟花括号{}包裹的C代码。这才是yacc的灵魂。当规则被匹配时这段代码就会执行用于构建语法树节点。例如select_statement: SELECT select_list FROM table_references { $$ new PT_select_statement($2, $4); // $2对应select_list的值$4对应table_references的值 thd-lex-current_select-parsing_place CTX_SELECT_LIST; }这里的$$代表本条规则select_statement的合成值$1、$2...代表各个组成部分的值。这些值通常是指向语法树节点的指针。错误恢复yacc支持定义error规则。MySQL中定义了复杂的错误恢复机制当解析出错时尝试同步到下一个分号;或FROM等关键字然后继续解析而不是立即终止。这保证了在一个存储过程或批处理语句中某条语句的语法错误不会影响其他语句的解析。一个重要的经验之谈修改sql_yacc.yy后需要重新运行BisonGNU版的yacc生成sql_yacc.cc。务必使用MySQL项目指定的Bison版本如3.x否则生成的解析器可能行为异常或存在兼容性问题。编译时可能会遇到大量的Shift/Reduce或Reduce/Reduce冲突警告需要仔细分析。理想的文法应该是无冲突的但SQL语言极其复杂MySQL的文法经过多年演化仍存在一些通过优先级规则巧妙化解的冲突。3. 从源码到解析树MySQL解析流程全景现在我们把Lex和yacc串起来看看一条SQL语句SELECT name FROM users WHERE id1在MySQL中是如何走完解析之旅的。3.1 解析入口与驱动解析的入口函数通常是mysql_parse在sql/sql_parse.cc。简化流程如下void mysql_parse(THD *thd, Parser_state *parser_state) { // 1. 初始化Lex状态机 lex_start(thd); thd-m_parser_state parser_state; // 2. 设置输入源来自网络包或文件 parser_state-m_lip.init(thd, parser_state-m_query_string.str, parser_state-m_query_string.length); // 3. 调用yacc生成的解析函数在sql_yacc.cc中 int parse_result yyparse(thd); // 4. 处理解析结果 if (parse_result 0) { // 解析成功thd-lex-result指向生成的语法树根节点 // 后续进行语义分析、优化等 } else { // 解析失败错误信息已通过yyerror设置到thd中 handle_parse_error(thd); } }yyparse就是yacc生成的解析器主函数。它会反复调用我们前面提到的MYSQLlex来获取Token并根据sql_yacc.yy中的规则进行归约执行对应的动作代码最终构建出完整的语法树。3.2 语法树Parse Tree节点结构MySQL的语法树节点是一个复杂的继承体系基类通常是Parse_tree_node。不同类型的语句和表达式有各自的派生类例如PT_select_stmt代表SELECT语句。PT_table_reference代表FROM子句中的表引用。PT_item代表表达式项如列、常量、函数调用这是一个庞大的家族包括PT_item_int、PT_item_string、PT_item_func等。在yacc的动作代码中创建的就是这些类的实例并通过父子指针连接成树。例如WHERE id1会被解析为一个PT_item_func_eq等于函数节点其两个子节点分别是代表列id的PT_item_field和代表常量1的PT_item_int。调试与观察技巧如果你想直观地看到生成的语法树可以在调试器中设置断点。一个常用的方法是在yyparse返回后查看thd-lex-current_select()或thd-lex-query_block。对于更底层的探索可以修改源码在动作代码中增加日志打印输出节点创建和链接的信息。不过这需要对MySQL的解析器数据结构有较深的理解。3.3 语义分析与上下文处理纯语法解析只检查结构是否正确但SQL的合法性更多取决于语义。这部分工作虽然主要在解析后阶段但有些已经在解析过程中交织进行了。符号表Symbol Table管理当解析到表名或列名时Lex只将其识别为IDENTIFIER。yacc的动作代码需要将这些标识符字符串暂存起来。后续的“名称解析”阶段会查询数据库的元数据系统表检查这些表、列是否存在用户是否有权限并将标识符字符串解析为具体的数据库对象ID。一个常见误区很多人以为解析阶段就完成了“找表找列”其实那属于语义分析解析阶段只负责记下“名字”。语句上下文Parsing Context解析器需要知道当前处于哪个上下文中因为同样的语法元素意义可能不同。例如SELECT (SELECT 1) 外层SELECT的上下文是CTX_SELECT_LIST内层子查询的上下文是CTX_SUBQUERY。SET var 1 这里的是赋值而在WHERE子句中是比较。 MySQL通过thd-lex-context等变量来跟踪这些状态yacc的动作代码会读取和设置这些状态以指导Lex进行正确的词法分析如前面提到的点号问题和后续的语义动作。4. 实战定位并解决一个解析器相关问题理论说得再多不如解决一个实际问题。假设我们在MySQL的error log里发现一条奇怪的错误You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near OPTION at line 1而执行的语句是SELECT * FROM t OPTION (MAX_EXECUTION_TIME1000);这是一个合法的查询优化器提示语法在特定版本后引入。4.1 问题排查思路版本与模式确认首先确认MySQL版本和sql_mode。OPTION关键字用于优化器提示是较新的语法如8.0.20可能在旧版本或不兼容的sql_mode下不被支持。追踪Token流如果怀疑是解析器问题最直接的方法是查看Lex到底把OPTION识别成了什么。我们可以通过源码调试或打日志。方法一调试在MYSQLlex函数中在返回Token的代码附近设断点观察yytext和返回的Token值。方法二源码分析搜索sql_yacc.yy和lex.h看OPTION被定义为什么Token以及它在哪些语法规则中出现。我们可能会发现在旧的文法中OPTION可能被定义为一个独立的、用于其他用途如SET OPTION语句的关键字而在新的优化器提示语法中它被用作一个“上下文关键字”其解析依赖于特定规则。4.2 模拟问题根源与修复假设我们发现在某个分支版本的sql_yacc.yy中规则有冲突/* 旧规则用于SET语句 */ set_option: OPTION option_value_list ; /* 新规则尝试引入用于SELECT的优化器提示 */ select_options: /* empty */ | select_options select_option ; select_option: OPTION ( optimizer_hint_list ) // 冲突OPTION在这里被用作提示开始 ;yacc可能会报告Reduce/Reduce冲突因为它无法确定当看到OPTION时应该归约到set_option的起始还是select_option的起始。解决方案通常需要重构文法。MySQL社区实际的解决方式可能更巧妙上下文判断在Lex识别OPTION时不直接返回一个固定的OPTION_TOKEN而是先查看上下文。如果当前在SELECT语句解析中并且后面跟着(则返回一个特殊的OPTION_HINT_TOKEN否则返回普通的OPTION_TOKEN用于SET语句。文法拆分在yacc中为这两个用途定义不同的非终结符和规则利用前面Lex提供的不同Token来消除歧义。操作记录如果我们想为自己的MySQL分支添加一个自定义的SQL语法扩展步骤通常是在lex.h中定义新的Token如MY_CUSTOM_TOKEN。在sql_yacc.yy的%token部分声明这个Token。在Lex的规则或相关函数中添加识别新语法关键词如MYKEYWORD并返回MY_CUSTOM_TOKEN的逻辑。在sql_yacc.yy的语法规则部分添加使用MY_CUSTOM_TOKEN的新规则并在动作代码中构建新的语法树节点。重新生成解析器bison -d sql_yacc.yy编译整个MySQL。在后续的语义分析、优化、执行阶段处理你新添加的语法树节点类型。注意修改核心解析器风险极高。任何文法冲突或错误的动作代码都可能导致解析器崩溃或行为错乱。务必在独立的开发分支上进行并编写大量的语法测试用例进行覆盖包括正确语句和预期会报错的语句。5. 常见问题与排查技巧实录即使不修改源码理解解析器也能帮你更好地应对日常开发中的问题。问题1为什么SELECT * FROM table报语法错误而SELECT * FROMtable加反引号就正确排查这几乎肯定是table是MySQL的保留关键字。在默认模式下Lex将其识别为TABLE_TOKEN一个关键字而非IDENTIFIER。yacc在FROM子句后期待一个表名标识符收到关键字Token就会报语法错误。用反引号包裹后Lex会将整个table识别为一个带引号的标识符Token。解决养成对表名、列名使用反引号的习惯尤其是在使用可能的关键字时。或者查询INFORMATION_SCHEMA.KEYWORDS表了解当前版本的关键字列表。问题2错误信息“near ‘某字符串’”指向的位置不准确。原因yacc的yylloc位置信息是由Lex在识别Token时提供的。如果Lex对注释、字符串常量、跨多行的标识符的处理逻辑有偏差导致行号、列号计算错误错误信息就会偏移。排查这通常是MySQL自身的bug。可以尝试简化SQL定位到触发错误的精确字符。如果能在最新版本复现可以向MySQL官方提交bug报告附上最小化复现案例。问题3存储过程或复杂视图解析极慢。排查除了常见的性能问题解析器也可能成为瓶颈尤其是当SQL语句巨大如包含超长的IN列表或嵌套层级极深时。yacc生成的LALR(1)解析器时间复杂度虽然是O(n)但巨大的语句会产生巨大的语法树消耗大量内存。诊断可以开启性能模式Performance Schema观察wait/synch/mutex/sql/LOCK_parse_tree之类的锁等待事件。或者使用SHOW PROFILE查看语句执行各阶段耗时关注“parsing”阶段。优化对于程序生成的SQL考虑拆分语句、使用参数化查询PREPARE代替直接拼接超长列表。参数化查询不仅安全而且同一模板只需解析一次解析结果解析树可以被缓存复用这就是MySQL的“查询缓存”虽然8.0中查询结果缓存被移除但语句解析树缓存仍然存在。问题4自定义函数或插件中如何安全地解析SQL片段建议不要直接调用mysql_parse或yyparse它们依赖完整的线程上下文THD和全局状态。正确的做法是使用MySQL提供的公开API如parse_sql如果可用或通过Services接口。更常见的模式是你的插件接收的是已经解析好的Item对象表达式树或Table_ref对象表引用你只需要处理这些对象而非原始SQL字符串。速查表解析器相关错误与可能方向错误现象可能原因初步排查方向“syntax error” 后跟的关键字是保留字标识符未用反引号转义检查表名/列名是否为关键字错误位置指向明显不对Lex位置计算错误检查SQL中是否有非常规字符或超长字符串特定复杂语句解析内存激增语法树过大检查语句是否包含数万级的IN列表或深度嵌套新版本MySQL不识别旧版本支持的语法文法已更新旧语法被移除查阅对应版本的官方变更日志Release Notes自定义SQL扩展编译后解析器崩溃文法存在冲突或动作代码有bug检查bison生成的.output文件中的冲突报告用简单语句逐步测试最后我想分享的一点个人体会是阅读MySQL的yacc和Lex源码最初可能会被其庞大的规模sql_yacc.yy超过上万行和复杂的宏定义所吓退。最好的切入方式不是通读而是带着具体问题去追踪。比如就想搞清楚LIMIT 10 OFFSET 20这个子句是怎么被解析的就从LIMIT这个Token在lex.h中的定义开始一路追踪到sql_yacc.yy中的limit_clause规则再到动作代码中看它如何构建PT_limit节点。像侦探一样沿着一条线索深挖下去你不仅会理解解析器更能窥见MySQL整个查询处理框架的精密与优雅。这个过程本身就是对计算机如何理解人类语言的一次深刻实践。
返回列表