开发ZGLanguage 去解析SQL数据血缘,还把复杂SQL脚本的结构图画出来,这事儿我前后折腾了小半年,中间推倒重来两次,踩坑无数。之前看到相关网络热词里有人在找 SQL 解析、数据血缘、源代码相关的方案,发现不少人卡在同一个地方:SQL 脚本一复杂,解析器就罢工,血缘关系画出来根本没法看。正好我最近把这套解析逻辑重写完了,趁热把整个设计思路、核心实现和踩坑记录整理出来,给后面做数据治理、数仓血缘这块的朋友一个可以抄作业的参考。
这个项目最终能做什么?输入一段 SQL(哪怕里面带了十几个 CTE、三层嵌套子查询、各种 join 和 union),ZGLanguage 会把 SQL 拆成一棵语法树,再从语法树里抽取出表级和字段级的数据血缘关系,最后把这些关系渲染成一张结构图。更关键的是,它能处理“一段脚本里有上千行 SQL”的场景,把这种级别的复杂脚本结构可视化成图,方便人眼快速定位某张表的数据是从哪一层哪条链路加工出来的。
适合谁看?想自己实现 SQL Parser 的人、被静态血缘工具坑过想搞一套轻量解析方案的人、还有数据治理方向想理解血缘分析原理的同学。这篇不是那种贴上去就完事的源码解析,我会把为什么要这么设计、每个关键选择背后的思考都讲透。
1. 这个项目的源头:为什么放着现成方案不用,非要写个新解析器
1.1 数据血缘压根不是“搜关键词”就能搞定的活
我在最开始犯过一个典型的错误:想用字符串匹配去拆 SQL,比如正则去抓 from table_a 这种片段,觉得只要把表名摘出来就能构建血缘了。这套逻辑处理十几行的简单 SQL 还行,但真实数仓里的 SQL 脚本往往长这样:开头是 set 参数,中间是七八个 CTE 叠在一起,正文里再套几个 case when,最后还有 insert overwrite。如果你用正则去抠表名,第一个 CTE 的表名可能还能抓到,但后面那些通过 with t1 as (...), t2 as (...) 串联的表,根本没法判断谁依赖谁。
换句话说,数据血缘提取的核心难点不在“能不能找到表”,而在“能不能正确识别表与表之间的依赖关系”。同一张表在 SQL 里出现三次,哪一次是上游的来源表,哪一次是下游的目标表?这个信息在字符串层面是不存在的,只有把 SQL 结构“解析”出来,才能做出准确的判断。这就是我下决心要写 ZGLanguage 的原因之一。
1.2 现成 SQL 解析方案的三座大山
动手之前我调研过一些现成方案,也给团队做过对比评估,当时的结论是各有各的难处。
第一类是直接基于数据库自身的血缘功能。比如数仓本身提供的血缘接口,或者某些商业 BI 工具带的血缘追溯。这类方案的好处是精确,毕竟数据库自己最懂自己的 SQL 方言。但问题也很明显:一是被厂商绑定,换一套引擎就得换一套血缘方案;二是绝大多数血缘能力只看得到表级,看不到字段级;三是离线批处理脚本、临时跑的数据同步任务,根本不经过数仓引擎,血缘自然无从记录。
第二类是找开源社区的 SQL 解析器来二次开发。像 JSqlParser、Apache Calcite、sqlglot、sqlparse 这些我都试过。实话实说,它们解析单条 SQL 的能力很强,Calcite 甚至能把SQL转成关系代数,做很深的优化分析。但对这个场景来说,它们有个共同的尴尬:它们更擅长解析,不擅长还原真实的数据加工语义。什么是加工语义?比如一张临时表其实是另一张表的字段做了 concat 之后的结果,字段级血缘到底应该链到上游表的哪个字段,这是需要结合业务去判断的,通用解析器不管这些。
第三类是用可视化 BI 工具自带的血缘视图。这类只适合做展示,连解析能力都不给你开放,想把它接进自己的数据治理平台根本无从下手。
1.3 ZGLanguage 的设计定位与整体工作流程
综合考虑了上面这些问题之后,我给 ZGLanguage 定的目标就很清晰了:做一个轻量级、可控、能让使用者二次开发的 SQL 血缘解析引擎。它不追求像 Calcite 那样做查询优化,也不追求像 JSqlParser 那样把每一条 SQL 的所有语法细节都解析到位,而是要抓住跟“血缘”相关的核心信息。
整个工作流程被切成了四段:
- 对输入的 SQL 脚本先做预处理,把注释、多余的空行、SET 参数先摘干净。
- 做词法分析,把原本是一长串字符的脚本切成 tokens,比如关键字、表名、字段名、括号、运算符。
- 做语法分析,根据 Tokens 构建出抽象语法树(AST),这棵树会把 SQL 里面的嵌套关系、依赖顺序全部还原成一个逻辑层面的结构。
- 遍历 AST,提取血缘关系,生成结构图需要的数据。
最后一步我会详细展开,因为血缘能不能画准,很大程度上取决于语法树建得是否合理,这是一个“地基决定上层建筑”的活。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心解析细节拆解:词法分析、语法分析与 AST 构建
2.1 词法分析器设计:先让机器“看懂”SQL
词法分析是整个流程里的第一步,也是我最初认为最简单、后来发现坑最多的一步。简单来说,词法分析就是输入一长串的 SQL 字符,输出一个 token 数组,每个token带着自己的类型和值。
那 token 类型怎么划分?我参考了 JavaCC、Antlr 这类工具的做法,但没用它们自动生成解析器,因为后期需要嵌入自己的血缘语义,手写的更容易控制。ZGLanguage 里的 token 类型大概是下面这样的基本盘:
KEYWORD:select、from、where、join、group by、order by、union、with 等保留字IDENTIFIER:表名、字段名、别名LITERAL:字符串常量、数字常量OPERATOR:=、>、<、+、-、*、/等PUNCTUATION:逗号、分号、括号EOF:结束标记
这里有一个非常容易踩的坑:SQL 方言里的关键字跟普通字符串之间没有强制的分隔符。比如 select 后面直接跟字段名,中间当然有空格,但如果有人写了 count(*)total,count(*) 和 total 之间到底怎么切?一般的词法规则是以空格和逗号为界,可一旦遇到 count(*)total 这种写法,就会出现歧义。我在处理这类情况时,加了一条小规则:当解析 token 时发现一个标识符紧跟着 ( 并且这个标识符是内置聚合函数名,就把它们绑定在一起作为一个函数 token处理。
还有一个常见问题是关键字与字段重名。比如有人建表时把 desc 或者 comment 当字段名用,语法上其实是允许的。这种情况下词法分析器如果不做额外判断,会把字段名当成关键字,直接导致后面
