4. SQL 解析引擎:ANTLR、Calcite、Druid SQL Parser 的原理与对比
做元数据血缘追踪,第一步就是要把 SQL 给“吃进去”并“消化掉”。说白了,你得让机器理解一条 SQL 语句到底在干什么——它从哪个表读数据,经过什么转换,最后写到哪里去。
这个“消化”过程,就是 SQL 解析。市面上主流的解析引擎有三家:ANTLR、Apache Calcite 和 Druid SQL Parser。我这些年做血缘项目,这三家都深度用过,各有各的脾气。今天咱们就掰开揉碎聊聊它们。
核心观点:没有最好的解析器,只有最适合你场景的解析器。选型前,先想清楚你要解析的 SQL 复杂度、性能要求、以及是否需要做查询优化。
4.1 解析引擎的核心原理:从文本到 AST
不管哪个引擎,解析 SQL 的流程都差不多。我画了张图,你先看个全貌:
嗯,这里要注意:血缘追踪主要用到的是语法分析阶段产出的 AST。语义分析虽然能帮你做更精准的字段级血缘,但前提是你得有完整的元数据信息。
4.2 ANTLR:最灵活的解析器生成器
ANTLR(Another Tool for Language Recognition)是个解析器生成工具。它不是专门为 SQL 设计的,但你可以用它生成任何语言的解析器。
我个人习惯把 ANTLR 比作“乐高”——它给你一堆积木(文法规则),你自己拼出想要的形状。Hive、Spark SQL 的解析器底层用的就是 ANTLR。
4.2.1 工作原理
ANTLR 的工作分两步:
- 定义文法文件(.g4):用 ANTLR 的语法描述 SQL 的规则
- 生成解析器代码:ANTLR 根据 .g4 文件生成 Java/Python/C# 等语言的解析器
举个例子,解析一条简单的 SELECT 语句:
// 简化的 SELECT 文法片段
grammar SimpleSQL;
selectStatement
: SELECT columnList FROM tableName whereClause? SEMICOLON?
;
columnList
: STAR
| columnName (COMMA columnName)*
;
tableName
: IDENTIFIER
;
whereClause
: WHERE condition
;
SELECT: 'SELECT';
FROM: 'FROM';
WHERE: 'WHERE';
STAR: '*';
COMMA: ',';
SEMICOLON: ';';
IDENTIFIER: [a-zA-Z_][a-zA-Z0-9_]*;
用这个文法,ANTLR 会生成一个解析器。你输入 SELECT id, name FROM users WHERE age > 18;,它就能吐出对应的 AST。
我的经验:ANTLR 的强项是灵活,但坑也不少。我曾经在解析 Hive SQL 时,因为 Hive 的语法太“野”(比如 LATERAL VIEW、EXPLODE),自己写的文法规则经常出现歧义。建议直接用社区维护的 antlr4-grammars 仓库里的 SQL 文法,别自己从头写。
4.2.2 优缺点
| 维度 | 说明 |
|---|---|
| 优点 | 高度灵活,支持自定义语法;社区生态好,有现成的 SQL 文法;支持多种目标语言 |
| 缺点 | 需要维护文法文件;对复杂 SQL(如嵌套子查询)解析性能一般;没有内置的元数据绑定能力 |
| 适用场景 | 需要解析非标准 SQL(如 Hive、Flink SQL);需要深度定制 AST 结构 |
4.3 Apache Calcite:全栈 SQL 处理框架
Calcite 是个“大块头”。它不只是一个解析器,更是一个完整的 SQL 处理框架——解析、校验、优化、执行计划生成,一条龙服务。
说白了,Calcite 把 SQL 解析当成它整个生态的一环。你用它解析 SQL,得到的不仅仅是 AST,还能直接拿到经过校验和类型绑定的关系代数表达式。
4.3.1 工作原理
Calcite 的解析流程是这样的:
- SQL → SqlNode(AST):Calcite 内置了基于 JavaCC 的解析器,将 SQL 解析成 SqlNode 树
- SqlNode → RelNode(关系代数):通过 SqlToRelConverter 将 AST 转换成关系代数表达式
- RelNode → 优化:基于 HepPlanner 或 VolcanoPlanner 进行规则优化
用代码看更直观:
// 使用 Calcite 解析 SQL
import org.apache.calcite.sql.*;
import org.apache.calcite.sql.parser.*;
public class CalciteParserDemo {
public static void main(String[] args) throws Exception {
String sql = "SELECT a.id, b.name " +
"FROM orders a " +
"JOIN users b ON a.user_id = b.id " +
"WHERE a.amount > 100";
// 1. 解析为 SqlNode
SqlParser parser = SqlParser.create(sql);
SqlNode sqlNode = parser.parseQuery();
// 2. 打印 AST 结构
System.out.println(sqlNode.toString());
// 3. 如果是 SqlSelect,可以获取各个子节点
if (sqlNode instanceof SqlSelect) {
SqlSelect select = (SqlSelect) sqlNode;
SqlNode from = select.getFrom();
SqlNode where = select.getWhere();
SqlNodeList selectList = select.getSelectList();
System.out.println("SELECT 字段: " + selectList);
System.out.println("FROM 子句: " + from);
System.out.println("WHERE 条件: " + where);
}
}
}
避坑指南:Calcite 的 SqlNode 结构比较“重”。我曾经在解析一条包含 20 个 JOIN 和 5 层子查询的 SQL 时,Calcite 直接抛了 StackOverflowError。原因是它的递归遍历太深。解决办法是调大 JVM 栈空间,或者改用迭代式遍历。
3.3.2 优缺点
| 维度 | 说明 |
|---|---|
| 优点 | 内置完整的 SQL 校验和优化能力;支持 SQL 标准全面;社区活跃,Flink、Druid 等都在用 |
| 缺点 | 学习曲线陡峭;解析性能不如轻量级方案;对非标准 SQL 支持有限 |
| 适用场景 | 需要做 SQL 优化或查询改写;需要字段级血缘且已有元数据信息 |
4.4 Druid SQL Parser:轻量级解析利器
Druid 是阿里巴巴开源的数据库连接池,但它里面藏了一个“彩蛋”——SQL Parser 模块。这个模块是专门为血缘追踪设计的,轻量、高效、开箱即用。
我记得第一次用 Druid Parser 时,感觉就一个字:爽。它不需要你定义文法,不需要配置元数据,直接传 SQL 进去就能拿到解析结果。
4.4.1 工作原理
Druid Parser 的核心是 SQLParserUtils 和 SQLStatement 体系:
// 使用 Druid 解析 SQL
import com.alibaba.druid.sql.*;
import com.alibaba.druid.sql.ast.*;
import com.alibaba.druid.sql.dialect.mysql.parser.*;
public class DruidParserDemo {
public static void main(String[] args) {
String sql = "INSERT INTO target_table (col1, col2) " +
"SELECT a.col1, b.col2 " +
"FROM source_table a " +
"JOIN lookup_table b ON a.key = b.key";
// 1. 创建 MySQL 解析器
MySqlStatementParser parser = new MySqlStatementParser(sql);
// 2. 解析为 SQLStatement
SQLStatement statement = parser.parseStatement();
// 3. 判断语句类型
if (statement instanceof SQLInsertStatement) {
SQLInsertStatement insert = (SQLInsertStatement) statement;
// 获取目标表
SQLExprTableSource tableSource = insert.getTableSource();
System.out.println("目标表: " + tableSource.getTableName());
// 获取查询部分
SQLSelect select = insert.getSelect();
if (select != null) {
SQLSelectQueryBlock query = (SQLSelectQueryBlock) select.getQuery();
// 遍历 SELECT 字段
for (SQLSelectItem item : query.getSelectList()) {
System.out.println("SELECT 字段: " + item.getExpr());
}
// 获取 FROM 表
SQLTableSource from = query.getFrom();
System.out.println("来源表: " + from);
}
}
}
}
4.4.2 优缺点
| 维度 | 说明 |
|---|---|
| 优点 | 轻量级,无外部依赖;解析速度快;对 INSERT/SELECT 等 DML 支持好;内置多种数据库方言 |
| 缺点 | 不支持 SQL 优化;对复杂 CTE、窗口函数支持不够完善;社区维护力度一般 |
| 适用场景 | 纯血缘追踪场景;需要快速解析大量 SQL;不需要做查询优化 |
4.5 三款引擎对比总结
我把三款引擎的核心差异整理成了一张表,方便你选型时参考:
| 维度 | ANTLR | Calcite | Druid Parser |
|---|---|---|---|
| 定位 | 解析器生成器 | SQL 处理框架 | 轻量级解析器 |
| 解析方式 | 文法驱动 | JavaCC + 自定义 | 手写解析器 |
| AST 结构 | 自定义 | SqlNode → RelNode | SQLStatement |
| SQL 标准支持 | 取决于文法 | 全面 | 常用语法 |
| 方言支持 | 需自行实现 | 内置多种 | MySQL、PG、Oracle 等 |
| 性能 | 中等 | 较慢 | 快 |
| 学习成本 | 高 | 高 | 低 |
| 血缘追踪推荐度 | ★★★☆☆ | ★★★★☆ | ★★★★★ |
4.6 实战:解析 INSERT/SELECT 语句提取血缘
好了,理论说完了,咱们来点实际的。假设你要解析下面这条 SQL,提取字段级血缘:
INSERT INTO dws.daily_sales (
product_id,
sale_date,
total_amount,
total_quantity
)
SELECT
p.product_id,
o.order_date,
SUM(o.amount) AS total_amount,
COUNT(o.quantity) AS total_quantity
FROM ods.orders o
JOIN dim.product p ON o.product_id = p.product_id
WHERE o.status = 'completed'
GROUP BY p.product_id, o.order_date;
用 Druid Parser 解析,核心逻辑是这样的:
// 伪代码:提取 INSERT/SELECT 血缘
public class LineageExtractor {
public LineageInfo extract(String sql) {
// 1. 解析 SQL
MySqlStatementParser parser = new MySqlStatementParser(sql);
SQLStatement stmt = parser.parseStatement();
LineageInfo lineage = new LineageInfo();
if (stmt instanceof SQLInsertStatement) {
SQLInsertStatement insert = (SQLInsertStatement) stmt;
// 2. 提取目标表
String targetTable = insert.getTableSource().getTableName();
lineage.setTargetTable(targetTable);
// 3. 提取 SELECT 查询
SQLSelect select = insert.getSelect();
SQLSelectQueryBlock query = (SQLSelectQueryBlock) select.getQuery();
// 4. 提取来源表
List<String> sourceTables = extractSourceTables(query.getFrom());
lineage.setSourceTables(sourceTables);
// 5. 提取字段映射
List<SQLExpr> targetColumns = insert.getColumns();
List<SQLSelectItem> sourceColumns = query.getSelectList();
for (int i = 0; i < targetColumns.size(); i++) {
String targetCol = targetColumns.get(i).toString();
String sourceExpr = sourceColumns.get(i).getExpr().toString();
// 记录字段级血缘
lineage.addColumnMapping(targetCol, sourceExpr);
}
}
return lineage;
}
}
我的建议:实际项目中,SQL 往往比这个复杂得多——有子查询、CTE、窗口函数、CASE WHEN 等等。我的做法是:先用 Druid Parser 做第一层解析,遇到它处理不了的语法,再 fallback 到 Calcite 或 ANTLR。这样既保证了性能,又覆盖了边界情况。
4.7 选型建议
最后,给你一个简单粗暴的选型建议:
- 如果你只需要做血缘追踪,不想折腾文法、不想管优化——选 Druid Parser,省心省力
- 如果你需要做 SQL 校验或查询改写,或者你的 SQL 特别复杂——选 Calcite,功能最全
- 如果你要解析非标准 SQL(比如 Hive、Flink、Spark SQL),或者你需要深度定制 AST——选 ANTLR,最灵活