4、SQL解析技术:Antlr解析、正则匹配、抽象语法树(AST)、自定义解析器

做数据血缘追踪,最核心的一步是什么?

我个人觉得,就是把SQL语句拆开,看懂它到底引用了哪些表、哪些字段。你想想看,一条写了八百行的ETL脚本,人工去翻,眼睛都得看花。所以我们必须借助工具来解析SQL。

这一章,我就跟你聊聊几种主流的SQL解析方式。有轻量的正则匹配,有重量级的Antlr,还有我们自己手撸的自定义解析器。每种方案都有它的脾气,选对了,事半功倍。

4.1 正则匹配:简单粗暴,但别太依赖

很多刚入行的同学,一上来就喜欢用正则表达式去抠SQL里的表名。比如匹配 FROM 或 JOIN 后面的单词。确实,写起来快,几行代码就能跑。

一个简单的正则示例:

import re

sql = "SELECT a.id, b.name FROM table_a a JOIN table_b b ON a.id = b.a_id"
pattern = r'(?:FROM|JOIN)\s+(\w+)'
tables = re.findall(pattern, sql, re.IGNORECASE)
print(tables)  # ['table_a', 'table_b']

看起来挺爽,对吧?但我得给你泼盆冷水。正则匹配在真实生产环境里,就是个“纸老虎”。

我曾经踩过的坑:

  • 子查询嵌套: 正则很难处理多层嵌套的 SELECT,比如 SELECT * FROM (SELECT ... FROM table_c) t,它可能只抓到最外层的别名 t,漏掉真正的 table_c。
  • CTE(公用表表达式): 遇到 WITH ... AS 这种写法,正则基本就懵了。
  • 关键字冲突: 如果字段名就叫 from_date,正则可能会误判。

所以我的建议是:正则匹配只适合做快速预览、日志过滤,或者解析那些格式极其规范的简单SQL。千万别拿它去做核心的血缘解析引擎,否则后期维护成本会让你想哭。

4.2 Antlr解析:工业级的选择

那正规军用什么?Antlr。这是一个语法分析器生成工具,它能根据你定义的语法规则,自动生成解析器。说白了,你给它一套“语法说明书”,它就能读懂任何符合这套语法的SQL。

Antlr的厉害之处在于,它能生成一棵抽象语法树(AST)。这棵树把SQL的层级结构、逻辑关系都保留下来了。我们做血缘分析,本质上就是在这棵树上做遍历,找到所有的表节点和字段节点。

Antlr解析流程:

  1. 词法分析: 把SQL字符串拆成一个个“单词”(),比如关键字、标识符、运算符。
  2. 语法分析: 根据语法规则,把这些组装成一棵树(AST)。
  3. 遍历AST: 我们写一个监听器(Listener)或访问器(Visitor),去遍历这棵树,提取血缘信息。

我记得在做一个银行的数据治理项目时,他们的SQL用了大量的Oracle方言,比如 CONNECT BY、MERGE INTO。用正则根本搞不定,最后就是用Antlr,下载了对应的SQL语法文件(比如 MySqlParser.g4 或 PlSqlParser.g4),稍微改改就能用。虽然学习曲线陡一点,但一劳永逸。

4.3 抽象语法树(AST):血缘追踪的地图

AST长什么样?你可以把它想象成一本SQL的“解剖图”。根节点是整个SQL语句,子节点是各种子句(SELECT、FROM、WHERE),叶子节点就是具体的表名、字段名、常量。

下面这张图,展示了一条简单SQL对应的AST结构:

SQL抽象语法树(AST)示例 SELECT 语句 SELECT 子句 FROM 子句 WHERE 子句 字段: a.id, b.name 表: table_a, table_b 条件: a.id = b.a_id 解析思路 遍历AST时,从FROM子句找到表名 再从SELECT子句找到字段名,建立映射关系

有了这棵树,事情就变得简单了。我们只需要写一个递归函数,从根节点往下走。遇到 FROM 节点,就记录它的子节点(表名);遇到 SELECT 节点,就记录它的子节点(字段名)。

伪代码示例:

def extract_lineage(ast_node):
    if ast_node.type == 'FROM':
        for child in ast_node.children:
            if child.type == 'TABLE':
                tables.append(child.name)
    elif ast_node.type == 'SELECT':
        for child in ast_node.children:
            if child.type == 'COLUMN':
                columns.append(child.name)
    # 递归遍历子节点
    for child in ast_node.children:
        extract_lineage(child)

4.4 自定义解析器:轻量级的“手术刀”

Antlr虽好,但有时候太重了。比如你只是要解析一种内部自定义的DSL(领域特定语言),或者你的SQL方言特别奇怪,Antlr的通用语法文件覆盖不了。这时候,自己写一个解析器反而更灵活。

我做过一个项目,需要解析一种类似SQL但又不是SQL的脚本,用来做数据脱敏规则配置。用Antlr得从头写语法文件,太费劲。最后我手写了一个递归下降解析器。

说白了,就是写几个函数,每个函数负责解析一种语法结构。比如:

  • parseSelect():处理 SELECT 关键字后面的内容。
  • parseFrom():处理 FROM 关键字后面的内容。
  • parseWhere():处理 WHERE 关键字后面的内容。

每个函数都像一个“小机器人”,只认自己负责的那一块。遇到不认识的就报错,或者跳过。

什么时候该自己写?

场景 推荐方案 理由
标准SQL(MySQL、Hive、SparkSQL) Antlr 社区有现成的语法文件,开箱即用
简单日志解析、临时分析 正则匹配 快速、低成本
内部DSL、非标准语法 自定义解析器 灵活,不受通用语法限制
高性能、低延迟场景 自定义解析器 可以针对特定语法做极致优化

4.5 实战中的选择建议

说了这么多,到底怎么选?我个人的经验是:

第一步,先看SQL的复杂度。 如果只是简单的 SELECT * FROM table,正则就够了。但只要你发现SQL里出现了子查询、CTE、窗口函数,立刻放弃正则,上Antlr。

第二步,看性能要求。 Antlr生成的解析器,性能其实不错。但如果你需要每秒解析上万条SQL,那可能得考虑用自定义解析器,或者用更底层的技术(比如用JavaCC,或者直接手写状态机)。

第三步,看团队能力。 如果团队里没人懂Antlr,强行上马,后期维护会很难受。这时候,一个写得好、注释全的自定义解析器,反而更靠谱。

避坑指南: 我曾经见过一个团队,用正则写了上千行代码来解析SQL,结果每次遇到新的SQL语法,就要改半天。最后整个模块变得极其脆弱,没人敢动。所以,选型的时候,一定要考虑未来的扩展性。别图一时爽,后面火葬场。