4. SQL 解析引擎:ANTLR、Calcite、Druid SQL Parser 的原理与对比

做元数据血缘追踪,第一步就是要把 SQL 给“吃进去”并“消化掉”。说白了,你得让机器理解一条 SQL 语句到底在干什么——它从哪个表读数据,经过什么转换,最后写到哪里去。

这个“消化”过程,就是 SQL 解析。市面上主流的解析引擎有三家:ANTLR、Apache Calcite 和 Druid SQL Parser。我这些年做血缘项目,这三家都深度用过,各有各的脾气。今天咱们就掰开揉碎聊聊它们。

核心观点:没有最好的解析器,只有最适合你场景的解析器。选型前,先想清楚你要解析的 SQL 复杂度、性能要求、以及是否需要做查询优化。

4.1 解析引擎的核心原理:从文本到 AST

不管哪个引擎,解析 SQL 的流程都差不多。我画了张图,你先看个全貌:

SQL 解析通用流程 词法分析 SQL → 流 语法分析 → AST 语义分析 AST → 校验/绑定 AST 各阶段产出物说明: • 词法分析:将 SQL 字符串拆成 ,比如 SELECT、FROM、table_name、WHERE 等 • 语法分析:根据文法规则,将 流组装成抽象语法树(AST) • 语义分析:校验表名、字段名是否存在,类型是否匹配,绑定元数据信息

嗯,这里要注意:血缘追踪主要用到的是语法分析阶段产出的 AST。语义分析虽然能帮你做更精准的字段级血缘,但前提是你得有完整的元数据信息。

4.2 ANTLR:最灵活的解析器生成器

ANTLR(Another Tool for Language Recognition)是个解析器生成工具。它不是专门为 SQL 设计的,但你可以用它生成任何语言的解析器。

我个人习惯把 ANTLR 比作“乐高”——它给你一堆积木(文法规则),你自己拼出想要的形状。Hive、Spark SQL 的解析器底层用的就是 ANTLR。

4.2.1 工作原理

ANTLR 的工作分两步:

  1. 定义文法文件(.g4):用 ANTLR 的语法描述 SQL 的规则
  2. 生成解析器代码: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 的解析流程是这样的:

  1. SQL → SqlNode(AST):Calcite 内置了基于 JavaCC 的解析器,将 SQL 解析成 SqlNode 树
  2. SqlNode → RelNode(关系代数):通过 SqlToRelConverter 将 AST 转换成关系代数表达式
  3. 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,最灵活