C++实现SQL语法树转换器:从词法分析到AST构建的完整实践 1. 项目概述与核心价值最近在做一个数据库中间件的性能优化需要深度解析用户传入的SQL语句来重写查询、做下推优化。直接操作字符串太原始了正则表达式写起来又复杂又容易出错还难以应对嵌套查询这种复杂结构。于是我决定自己动手用C实现一个从SQL语句到语法树的转换器。这听起来像编译器前端的活儿没错它的核心思路和编译器里的词法分析、语法分析一脉相承但目标更聚焦不是为了生成机器码而是为了得到一个结构化的、能清晰表达SQL语义的中间表示IR也就是语法树AST。这个转换器能做什么简单说它把像SELECT name, age FROM users WHERE age 18 ORDER BY age DESC这样一串文本变成一个内存中的树状数据结构。树上的每个节点都代表SQL语法中的一个元素SELECT子句、FROM子句、一个表达式、一个标识符等等。有了这棵树后续的操作就方便多了你可以遍历它来检查语义比如表名、列名是否存在可以修改它来优化查询比如把WHERE条件中的常量表达式提前计算甚至可以把它转换成另一种数据库方言的SQL或者生成执行计划。对于想深入理解数据库工作原理、开发数据库工具、或者构建需要解析SQL的应用程序比如报表系统、数据治理平台的开发者来说掌握这套技术是基本功。2. 整体架构与设计思路拆解2.1 为什么选择“词法分析 - 语法分析 - 构建AST”的经典路径处理结构化文本尤其是像SQL这种有严格语法定义的语言业界成熟且几乎唯一的选择就是编译原理中的前端技术栈。词法分析器Lexer负责把字符流切分成一个个有意义的“单词”Token比如把SELECT识别为关键字把name识别为标识符把识别为操作符把18识别为整数常量。这步消除了空白字符、注释的干扰将原始文本转化为更易于处理的Token序列。语法分析器Parser则根据预定义的语法规则通常用上下文无关文法描述检查Token序列是否符合SQL的语法结构并在这个过程中建立起语法树。我选择的是递归下降分析法因为它直观、易于手工实现和调试特别适合SQL这种语法相对复杂但又不是极度复杂的场景。相比于LR分析器需要生成复杂的状态表递归下降的代码结构几乎就是语法规则的直译可读性极强。2.2 核心组件职责与交互流程整个转换器的核心是三个模块的管道式协作词法分析器 (Lexer)输入是std::string或std::istream输出是Token流。它需要高效地逐个字符扫描识别Token类型和值并处理字符串、数字、标识符、关键字和操作符。语法分析器 (Parser)持有Lexer的引用消费Token流。它包含一系列递归函数每个函数对应语法中的一个非终结符如parseSelectStatement,parseExpression。Parser是驱动者它调用Lexer获取Token根据当前Token预测该调用哪个解析函数并在解析过程中创建AST节点。抽象语法树 (AST) 节点体系这是一套用C类或结构体定义的层次结构。根节点可能是SelectStatement它包含指向SelectClause、FromClause、WhereClause等子节点的指针。这些子节点可能又包含更细粒度的节点如Column、BinaryExpression表示age 18等。设计良好的AST节点类是后续所有操作的基础。整个流程始于用户输入SQL字符串Lexer将其化为Token流Parser消化Token流并调用节点构造函数搭建AST最终返回AST的根节点指针。后续的语义分析、优化、转换都可以基于这个根节点展开。2.3 关键设计决策手工实现 vs. 工具生成这里有个经典抉择是用Lex/Flex和Yacc/Bison这类生成器还是纯手工编写对于这个项目我选择了手工实现主要基于以下几点考量依赖与复杂度引入Flex/Bison会增加构建系统的复杂度对于希望保持轻量、易于集成的项目来说纯C代码是更干净的选择。调试友好性手工编写的Lexer和Parser你可以在任何地方设置断点单步跟踪Token的消耗和AST节点的创建过程逻辑一目了然。生成器的代码通常比较晦涩调试时需要映射回语法规则文件。性能与控制力手工实现允许你做极致的优化比如定制Token的缓存策略、实现特定的错误恢复机制。虽然对于大多数SQL场景生成器的性能也足够好但手动控制的感觉更踏实。学习价值亲手实现一遍对词法、语法分析的理解会深刻得多这是使用工具无法替代的体验。当然如果SQL语法极其庞大复杂要支持完整的SQL-92或更高标准使用生成器来维护语法规则可能更高效。但对于一个聚焦核心子集如SELECT、INSERT、UPDATE、DELETE及常用表达式的解析器手工实现是完全可控且富有成就感的。3. 核心细节解析与实操要点3.1 词法分析器从字符到Token的精准切割词法分析器的核心是一个状态机。我实现了一个Lexer类主要接口是一个nextToken()函数每次调用返回下一个Token。Token的设计struct Token { enum Type { KEYWORD_SELECT, KEYWORD_FROM, KEYWORD_WHERE, // 关键字 IDENTIFIER, // 标识符表名、列名 INTEGER_LITERAL, STRING_LITERAL, // 字面量 OPERATOR_PLUS, OPERATOR_GT, OPERATOR_EQ, // 操作符 COMMA, LEFT_PAREN, RIGHT_PAREN, // 分隔符 END_OF_FILE, // 文件结束 UNKNOWN // 未知字符 }; Type type; std::string lexeme; // Token对应的原始字符串 size_t line; // 行号用于错误定位 size_t column; // 列号 };lexeme字段很重要它保留了标识符的名字、数字的值等原始信息供后续阶段使用。关键识别逻辑跳过空白与注释在nextToken()开始用一个循环跳过空格、制表符、换行符。支持--单行注释和/* */多行注释也是在这里处理直接忽略掉注释内容即可。识别数字如果当前字符是数字进入数字识别状态。持续读取后续的数字字符直到遇到非数字字符。这里可以扩展支持小数点和科学计数法如3.14e-2。识别标识符和关键字如果当前字符是字母或下划线进入标识符识别状态。持续读取字母、数字、下划线。读取完成后得到的字符串需要去关键字表里查找。我通常用一个std::unordered_mapstd::string, Token::Type来存储关键字映射。如果查到了返回对应的关键字Token否则返回标识符Token。识别字符串遇到单引号进入字符串识别状态。需要特别处理转义字符比如\代表一个单引号字符本身\\代表反斜杠。一直读取到配对的结束单引号。识别操作符和分隔符对于,-,*,/,,,(,)等单字符操作符/分隔符直接返回。对于,,,!等可能组合成,,!,的需要“向前看”一个字符peek来决定。注意字符的“向前看”peek操作非常关键。Lexer需要维护一个“当前字符”指针或索引以及一个从输入流中预读下一个字符但不移动指针的函数。这能优雅地处理多字符操作符和区分与。实操心得性能小技巧频繁的字符串构造和销毁会影响性能。对于标识符和字面量可以考虑使用“字符串驻留”String Interning技术即维护一个全局的字符串池相同的字符串只存储一份Token中只保存指向池中字符串的指针或索引。这能大幅减少内存分配和字符串比较的开销。错误处理在词法分析阶段遇到无法识别的字符如、#在简单SQL中时不要直接崩溃。可以将其标记为UNKNOWN类型的Token并记录错误信息交给语法分析阶段或统一的错误收集器处理。这样能一次报告多个词法错误。3.2 语法分析器递归下降与AST构建语法分析器是大脑。我定义了一个Parser类它持有Lexer实例和一个“当前Token”。核心方法是parseStatement()它根据第一个Token判断语句类型然后分发给具体的解析函数。语法规则定义以SELECT为例 我们首先需要定义要支持的SQL子集文法。例如一个简单的SELECT语句可以描述为SelectStatement - SELECT SelectList FROM TableName [WHERE Expression] [ORDER BY OrderList] SelectList - ColumnName (, ColumnName)* | * Expression - Term (( | | ) Term)? Term - INTEGER_LITERAL | IDENTIFIER OrderList - ColumnName [ASC|DESC] (, ColumnName [ASC|DESC])*这只是极度简化的示例实际表达式会复杂得多包括算术运算、函数调用、逻辑运算AND, OR, NOT等。递归下降函数实现 每个语法规则对应一个解析函数。函数内部根据当前Token决定匹配哪个产生式并递归调用其他解析函数。// 解析SELECT语句 std::unique_ptrSelectStatement Parser::parseSelectStatement() { auto stmt std::make_uniqueSelectStatement(); consume(Token::KEYWORD_SELECT); // 消耗掉SELECT关键字 stmt-selectList parseSelectList(); // 解析选择列表 consume(Token::KEYWORD_FROM); stmt-tableName parseTableName(); // 解析表名 // 看看后面有没有WHERE if (currentToken.type Token::KEYWORD_WHERE) { consume(Token::KEYWORD_WHERE); stmt-whereClause parseExpression(); // 解析WHERE条件表达式 } // 看看后面有没有ORDER BY if (currentToken.type Token::KEYWORD_ORDER) { consume(Token::KEYWORD_ORDER); consume(Token::KEYWORD_BY); stmt-orderByList parseOrderByList(); } return stmt; } // 解析表达式简化版只处理比较 std::unique_ptrExpression Parser::parseExpression() { auto left parseTerm(); // 解析左操作数 // 如果下一个Token是比较操作符 if (currentToken.type Token::OPERATOR_GT || currentToken.type Token::OPERATOR_EQ || currentToken.type Token::OPERATOR_LT) { auto op currentToken.type; consume(currentToken.type); // 消耗操作符 auto right parseTerm(); // 解析右操作数 // 创建一个二元表达式节点 return std::make_uniqueBinaryExpression(std::move(left), op, std::move(right)); } // 如果不是比较操作符可能就是一个简单的项如一个列名 return left; }关键函数解析consume(Token::Type expectedType)这是Parser的核心辅助函数。它检查当前Token的类型是否与期望的expectedType一致。如果一致就调用lexer.nextToken()获取下一个Token更新“当前Token”如果不一致就抛出一个语法错误指出在当前位置期望看到什么Token却看到了什么。peekToken()或lookahead(int k)有时需要查看后面第k个Token才能做出决策比如区分-是负号还是减号。这需要Lexer支持或者Parser维护一个小的Token缓冲区。AST节点设计示例// 表达式基类 class Expression { public: virtual ~Expression() default; // 可以添加accept方法用于访问者模式方便后续遍历 virtual void accept(ASTVisitor visitor) 0; }; // 二元表达式节点 class BinaryExpression : public Expression { public: std::unique_ptrExpression left; Token::Type op; // 操作符类型 std::unique_ptrExpression right; BinaryExpression(std::unique_ptrExpression l, Token::Type o, std::unique_ptrExpression r) : left(std::move(l)), op(o), right(std::move(r)) {} void accept(ASTVisitor visitor) override { visitor.visit(*this); } }; // 标识符表达式节点列名 class IdentifierExpression : public Expression { public: std::string name; IdentifierExpression(const std::string n) : name(n) {} void accept(ASTVisitor visitor) override { visitor.visit(*this); } }; // SELECT语句节点 class SelectStatement { public: std::unique_ptrSelectList selectList; std::string tableName; // 简化实际可能是多表或子查询 std::unique_ptrExpression whereClause; std::vectorOrderByElement orderByList; };注意使用std::unique_ptr来管理AST节点的生命周期是推荐做法它能清晰地表达所有权关系避免内存泄漏。当AST不再需要时只需释放根节点的unique_ptr整棵树会自动递归释放。3.3 错误处理与恢复策略一个健壮的解析器必须能优雅地处理错误。我们的策略是快速发现错误精准报告位置并尝试恢复解析以发现更多错误。错误报告每个Token和AST节点都应记录行号、列号。当consume函数发现Token不匹配时抛出一个包含位置信息和期望/实际Token详情的异常或者将错误添加到一个错误列表。错误恢复恐慌模式在递归下降中当在一个函数内遇到语法错误时不能直接退出整个解析。常见的恢复策略是“恐慌模式”跳过一些Token直到遇到一个“同步点”synchronization point。对于SQL同步点可以是分号;语句结束或者像FROM、WHERE、ORDER BY这样的关键字。Parser可以捕获异常记录错误然后尝试将输入流同步到下一个同步点并继续解析后续的语句。这样一次运行可以报告所有语法错误而不是第一个错误就停止。4. 完整实现流程与核心代码剖析4.1 第一步搭建项目结构与基础类创建一个C项目我习惯的目录结构如下sql_parser/ ├── include/ │ ├── token.h │ ├── lexer.h │ ├── ast.h │ └── parser.h ├── src/ │ ├── lexer.cpp │ ├── parser.cpp │ ├── ast.cpp │ └── main.cpp (用于测试) ├── CMakeLists.txt └── test.sql (测试用例)首先实现token.h和token.cpp定义完整的Token枚举和结构体。接着实现lexer.h和lexer.cpp完成Lexer类。在实现Lexer时务必编写充分的单元测试用各种边界情况的SQL片段验证Token切割是否正确。4.2 第二步实现AST节点体系在ast.h中用继承体系定义所有AST节点。如上文示例定义Expression基类以及各种具体的表达式节点BinaryExpression,IdentifierExpression,LiteralExpression,FunctionCallExpression等。然后定义语句节点如SelectStatement,InsertStatement等。每个节点类主要包含数据成员和构造函数以及一个可选的accept方法用于访问者模式。实操心得访问者模式Visitor Pattern是遍历和操作异构AST的神器。在ast.h中声明一个ASTVisitor抽象类为每种具体的AST节点定义一个visit虚函数。然后在每个AST节点类中实现accept方法调用visitor.visit(*this)。这样后续的语义检查器、优化器、代码生成器都可以通过实现ASTVisitor接口来遍历AST而无需修改AST节点类本身符合开闭原则。4.3 第三步实现递归下降语法分析器在parser.h和parser.cpp中实现Parser类。核心是parse()方法它调用parseStatement()。parseStatement()根据首个Token可能是SELECT,INSERT,UPDATE,DELETE分发到具体的解析函数。以解析SELECT列表为例的详细代码// 解析选择列表例如*, name, age, COUNT(*) std::unique_ptrSelectList Parser::parseSelectList() { auto selectList std::make_uniqueSelectList(); // 处理 * if (currentToken.type Token::OPERATOR_MUL) { selectList-isAllColumns true; consume(Token::OPERATOR_MUL); // 如果后面紧跟逗号语法错误SELECT *, column ... 在标准SQL中通常非法 if (currentToken.type Token::COMMA) { reportError(Cannot mix * with other columns in select list without explicit table alias.); } return selectList; } // 处理列列表 column1, column2, func(column3) AS alias selectList-isAllColumns false; do { // 解析一个选择项可能是一个列、一个表达式或一个函数调用 auto item parseSelectItem(); selectList-items.push_back(std::move(item)); // 如果下一个是逗号继续解析下一个项 if (currentToken.type Token::COMMA) { consume(Token::COMMA); } else { break; // 没有逗号了选择列表结束 } } while (true); return selectList; } std::unique_ptrSelectItem Parser::parseSelectItem() { auto item std::make_uniqueSelectItem(); // 首先尝试解析一个表达式可能是列名、函数调用、算术运算等 item-expression parseExpression(); // 检查是否有 AS alias if (currentToken.type Token::KEYWORD_AS) { consume(Token::KEYWORD_AS); if (currentToken.type ! Token::IDENTIFIER) { reportError(Expected identifier after AS); } item-alias currentToken.lexeme; consume(Token::IDENTIFIER); } else if (currentToken.type Token::IDENTIFIER) { // 有些SQL方言允许省略AS直接跟别名 // 这里需要小心可能是下一个列名。通常需要更多前瞻来判断。 // 简化处理如果当前表达式是一个简单的标识符且下一个Token也是标识符可能意味着是别名 // 更安全的做法是只支持带AS的别名。 // 我们这里先按必须AS处理所以不进入这个分支。 } return item; }表达式解析的优先级处理 表达式解析是难点因为操作符有优先级乘除高于加减比较高于逻辑AND/OR。一种清晰的方法是使用“攀爬法”Pratt Parsing或者为每个优先级层级编写一个解析函数。// 解析表达式处理逻辑OR优先级最低 std::unique_ptrExpression Parser::parseExpression() { return parseLogicalOR(); } // 解析逻辑OR (|| 或 OR) std::unique_ptrExpression Parser::parseLogicalOR() { auto left parseLogicalAND(); while (currentToken.type Token::KEYWORD_OR || (currentToken.type Token::OPERATOR_CONCAT your_lexer_supports_it)) { auto op currentToken.type; consume(op); auto right parseLogicalAND(); left std::make_uniqueBinaryExpression(std::move(left), op, std::move(right)); } return left; } // 解析逻辑AND ( 或 AND) std::unique_ptrExpression Parser::parseLogicalAND() { auto left parseEquality(); while (currentToken.type Token::KEYWORD_AND) { auto op currentToken.type; consume(op); auto right parseEquality(); left std::make_uniqueBinaryExpression(std::move(left), op, std::move(right)); } return left; } // 解析相等性比较 (, !, , , , , ) std::unique_ptrExpression Parser::parseEquality() { auto left parseAdditive(); while (currentToken.type Token::OPERATOR_EQ currentToken.type Token::OPERATOR_LE) { auto op currentToken.type; consume(op); auto right parseAdditive(); left std::make_uniqueBinaryExpression(std::move(left), op, std::move(right)); } return left; } // 解析加减法 (,-) std::unique_ptrExpression Parser::parseAdditive() { auto left parseMultiplicative(); while (currentToken.type Token::OPERATOR_PLUS || currentToken.type Token::OPERATOR_MINUS) { auto op currentToken.type; consume(op); auto right parseMultiplicative(); left std::make_uniqueBinaryExpression(std::move(left), op, std::move(right)); } return left; } // 解析乘除法 (*, /, %) std::unique_ptrExpression Parser::parseMultiplicative() { auto left parsePrimary(); while (currentToken.type Token::OPERATOR_MUL || currentToken.type Token::OPERATOR_DIV || currentToken.type Token::OPERATOR_MOD) { auto op currentToken.type; consume(op); auto right parsePrimary(); left std::make_uniqueBinaryExpression(std::move(left), op, std::move(right)); } return left; } // 解析基本单元标识符、字面量、括号表达式、函数调用等 std::unique_ptrExpression Parser::parsePrimary() { switch (currentToken.type) { case Token::IDENTIFIER: { // 可能是列名也可能是函数名 std::string name currentToken.lexeme; consume(Token::IDENTIFIER); // 看看后面是不是左括号如果是就是函数调用 if (currentToken.type Token::LEFT_PAREN) { return parseFunctionCall(name); } // 否则就是普通的列标识符 return std::make_uniqueIdentifierExpression(name); } case Token::INTEGER_LITERAL: { int64_t value std::stoll(currentToken.lexeme); consume(Token::INTEGER_LITERAL); return std::make_uniqueLiteralExpression(value); } case Token::STRING_LITERAL: { std::string value currentToken.lexeme; // 注意lexeme包含了引号可能需要去除 consume(Token::STRING_LITERAL); return std::make_uniqueLiteralExpression(value); } case Token::LEFT_PAREN: { consume(Token::LEFT_PAREN); auto expr parseExpression(); // 递归解析括号内的表达式 consume(Token::RIGHT_PAREN); return expr; } default: reportError(Expected identifier, literal, or ( in expression); return nullptr; // 错误恢复时可以返回一个空节点或抛出异常 } }通过这样一层层递归调用就能自然地处理操作符优先级。parsePrimary是递归的终点处理最基本的表达式单元。4.4 第四步集成测试与可视化调试在main.cpp中编写测试代码读取SQL文件或字符串调用Lexer和Parser最终得到AST根节点。一个简单的测试#include parser.h #include iostream #include fstream #include sstream int main() { std::string sql SELECT id, name, salary * 1.1 AS new_salary FROM employees WHERE department IT AND salary 50000 ORDER BY new_salary DESC; std::istringstream stream(sql); Lexer lexer(stream); Parser parser(lexer); try { auto stmt parser.parse(); if (stmt) { std::cout SQL parsed successfully! std::endl; // 可以在这里打印AST或进行后续处理 // 例如用一个简单的PrintVisitor来打印树结构 PrintVisitor visitor; stmt-accept(visitor); } } catch (const ParseException e) { std::cerr Parse error at line e.line , column e.column : e.what() std::endl; } catch (const std::exception e) { std::cerr Error: e.what() std::endl; } return 0; }可视化调试技巧 纯文本的AST打印不直观。可以借助Graphviz的DOT语言来生成AST的图形。实现一个DotVisitor继承自ASTVisitor在遍历每个节点时输出DOT格式的节点和边定义。最后将输出的文本保存为.dot文件用dot命令生成PNG或SVG图片。class DotVisitor : public ASTVisitor { public: std::ostream out; int nodeCounter 0; std::mapconst void*, int nodeIds; DotVisitor(std::ostream os) : out(os) { out digraph AST {\n; out node [shapebox, fontname\Courier\];\n; } ~DotVisitor() override { out }\n; } void visit(SelectStatement stmt) override { int id getNodeId(stmt); out N id [label\SelectStatement\];\n; // 递归访问子节点并创建边 if (stmt.selectList) { stmt.selectList-accept(*this); out N id - N getNodeId(stmt.selectList.get()) ;\n; } // ... 处理其他子节点 } // ... 为其他节点类型实现visit方法 private: int getNodeId(const void* ptr) { if (!nodeIds.count(ptr)) { nodeIds[ptr] nodeCounter; } return nodeIds[ptr]; } };在测试程序中生成AST后调用DotVisitor就能得到一张清晰的语法树图对于调试复杂SQL的解析结果非常有帮助。5. 常见问题、性能优化与扩展方向5.1 常见问题与排查技巧Tokenization错误标识符或关键字识别不准现象user_name被错误地切成user和_name两个Token。排查检查Lexer识别标识符的规则确保下划线_被包含在允许的字符集中。同时确保关键字表在识别完整个标识符字符串后才进行查找避免user被误认为关键字。技巧在Lexer的nextToken函数中添加详细的调试日志输出每个识别出的Token的类型和lexeme这是定位词法问题最快的方法。语法错误恢复后陷入死循环现象遇到一个错误后Parser跳过了大量Token但始终找不到同步点导致无限循环。排查检查恐慌模式恢复逻辑。确保同步点集合设置合理如FROM,WHERE,GROUP BY,ORDER BY,LIMIT,;。在恢复循环中需要持续消耗Token直到遇到同步点或文件结束。技巧在恢复循环中添加计数器如果跳过的Token超过一个阈值比如100个可以强制终止并报告“无法恢复的语法错误”避免死循环。内存泄漏现象程序长时间运行后内存持续增长。排查确保所有AST节点都使用智能指针如std::unique_ptr管理。如果使用了裸指针必须在析构函数或适当位置手动删除。使用Valgrind或AddressSanitizer等工具进行内存检查。技巧为AST节点基类定义虚析构函数。如果节点之间存在环形引用虽然SQL AST中不常见考虑使用std::shared_ptr和std::weak_ptr。解析速度慢现象解析一个很长的SQL文件或大量短SQL时感觉慢。排查瓶颈可能在Lexer的字符逐个读取或Parser中频繁的字符串比较关键字查找、AST节点构造。优化Lexer使用std::string_view来避免子字符串拷贝。如果SQL字符串很大可以考虑内存映射文件。关键字查找使用std::unordered_map或编译期哈希表如C17的std::string_view键值进行O(1)查找。AST节点分配如果性能要求极高可以考虑使用对象池Memory Pool来批量分配和回收节点减少new/delete的开销。5.2 性能优化进阶当需要处理海量SQL或对延迟极其敏感时可以考虑以下优化手写词法分析器虽然我们已经是手写的但可以进一步优化状态机使用查表法Table-Driven或直接编码Direct Coding来减少分支判断。避免字符串拷贝在整个解析流程中尽量使用std::string_view来引用原始SQL字符串中的片段而不是创建新的std::string。这要求原始SQL字符串在AST生命周期内保持有效。使用arena分配器为整个AST的创建预分配一大块连续内存arena所有节点都从这块内存中分配。销毁时直接释放整个arena效率极高。这牺牲了单独释放节点的灵活性但适合解析后整体使用和销毁的场景。缓存解析结果如果同一SQL语句会被反复解析例如在预编译语句中可以设计一个LRU缓存以SQL字符串的哈希值为键缓存对应的AST。但要注意SQL中可能包含字面量SELECT * FROM t WHERE id1和SELECT * FROM t WHERE id2虽然结构相同但字面量不同缓存时需要决定是否参数化。5.3 功能扩展方向一个基础的SQL解析器搭建完成后可以朝多个方向扩展支持更完整的SQL语法逐步添加对JOININNER,LEFT,RIGHT、子查询在FROM、WHERE、SELECT列表中、集合操作UNION,INTERSECT、GROUP BY、HAVING、LIMIT/OFFSET等的支持。每增加一个语法特性就在AST中增加相应的节点类型并在Parser中添加对应的解析函数。语义分析Semantic Analysis在AST基础上进行语义检查。这需要引入“符号表”Symbol Table的概念。遍历AST收集所有出现的表名、列名、别名检查它们是否存在需要连接数据库元数据或假设已存在、是否歧义如多表查询时未指定表名的列、类型是否匹配如对字符串列进行算术运算。语义分析器是连接语法和真实世界数据模型的桥梁。查询优化这是数据库的核心。基于AST可以进行许多优化例如常量折叠将WHERE salary 500001000优化为WHERE salary 51000。谓词下推将过滤条件尽可能推到靠近数据源的地方。查询重写将SELECT * FROM (SELECT * FROM t) AS sub优化为SELECT * FROM t。实现优化器需要定义一套转换规则并在AST上应用这些规则。生成目标代码将AST转换为其他形式。例如转换为另一个数据库的SQL方言或者转换为一个可执行的查询计划树用于你自己的数据库引擎甚至转换为某种中间代码如LLVM IR进行JIT编译执行。这通常通过实现不同的ASTVisitor来完成。实现一个SQL到语法树的转换器就像亲手搭建了一座连接人类可读的声明式语言与机器可处理的结构化数据之间的桥梁。这个过程充满了对细节的打磨和对复杂度的驾驭每一次成功解析一条复杂SQL所带来的成就感是直接使用现成库无法比拟的。从简单的SELECT开始逐步扩展到支持JOIN、子查询、窗口函数看着自己的解析器一点点强大起来这种体验对于深入理解数据库和编译技术至关重要。