做数据治理的朋友应该都有同感:数据血缘解析这活儿,看起来是“把SQL拆开看看”,真正做起来全是坑。尤其是UPDATE语句,很多血缘工具要么压根不支持,要么解析出来的结果图完全没法看。最近这段时间,我一直在折腾自研的解析工具ZGLanguage,专门用来做SQL数据血缘的解析和可视化,核心诉求就是把UPDATE SQL的结构图准确画出来。这篇文章,就把我在源代码层面踩过的坑、设计思路和实测效果完整梳理一遍,适合正在做数仓血缘、数据资产盘点,或者想自己写一个SQL解析器的朋友参考。
ZGLanguage这个名字,听着像是一门语言,其实它是一个以SQL解析为入口、以血缘关系为输出的工具项目,代码以源码形式维护。我给它定的目标是:不依赖任何商业产品,拿到一条UPDATE语句,就能自动产出两张图——一张是目标表怎么被更新的关系图,另一张是更新字段的数据来源图。这个能力在数仓里特别实用,你想想,ods层的表被更新,下游报表的数据来源说不清楚,出了问题连排查方向都没有。下面我按自己的实现顺序,从设计到编码,再到实测,完整讲一遍。
1. 为什么单独把 UPDATE 拎出来讲
1.1 大多数血缘工具“重SELECT轻UPDATE”
我刚开始做血缘的时候,第一反应是找现成的开源工具。试了一圈下来发现,绝大多数工具都把重心放在SELECT上,因为大家默认“查询才是分析入口”。SELECT的FROM、JOIN、GROUP BY都非常规整,解析器只要沿着语法树往下走就行。但UPDATE语句的处理能力,普遍弱得让人头疼,很多工具只做了“识别到UPDATE,就把整张表标记为被更新”,至于更新列的数据来自哪、经过了什么计算、关联了哪些其他表,一概不深究。
这种缺失带来的问题很现实。举个例子:一张dwd层的订单明细表,每天凌晨会被一个UPDATE任务回刷状态字段,这个UPDATE的子查询里关联了十张维表。如果血缘工具只知道“订单明细表被更新了”,却不知道它依赖了哪十张维表,那么当某张维表数据出错时,你根本不可能通过血缘图反向定位到根因。我见过不少团队为了解决这个问题,直接放弃自动解析,让人工维护一张excel血缘清单,结果名单更新速度永远赶不上SQL变更速度。
ZGLanguage把UPDATE单独拎出来做,就是想填补这个空缺。它不追求“看起来能用”,而是要把UPDATE所有可能的来源关系都拆出来,细到字段级,再画成结构图。这样无论是数仓同事核对逻辑,还是排查数据质量问题,都能直接看图说话。
1.2 UPDATE 血缘到底难在哪
先说一个很多人容易忽略的点:UPDATE语句的“目标”和“来源”是高度纠缠的。普通SELECT里,输入表和输出表边界很清晰,但UPDATE先要定位目标表,再在SET里引用同一张表或其他表的列。比如UPDATE t SET t.amount = t.amount + s.amount FROM s WHERE t.id = s.id这句话里,t.amount既是旧值来源,又是新值目标,同类列混在一起,如果解析器不区分“读取”和“写入”,血缘图上就会画出一条自环。
更麻烦的是来源表达式的多样性。SET右侧可以是裸列名,可以是函数调用,可以是CASE WHEN,还可以是一整条子查询。UPDATE t SET col = (SELECT max(x) FROM s WHERE s.id = t.id)这种写法在业务SQL里非常常见,子查询里的s.id = t.id还牵涉到关联条件,解析时既要抽字段血缘,又不能把WHERE里的过滤条件当作字段来源,否则图上会出现大量假血缘。
除了语法本身,方言差异也够喝一壶的。MySQL支持UPDATE t JOIN s ON ... SET t.c = s.c,SQL Server习惯写UPDATE t SET t.c = s.c FROM t INNER JOIN s ON ...,PostgreSQL又要求目标表前面不能带JOIN。同一个业务逻辑,在不同数据库里写法完全不同,解析器如果只认一种写法,换个环境就废了。后面我会详细说ZGLanguage是怎么用“统一AST层”来吸收这些差异的。
2. ZGLanguage 的整体设计思路
2.1 从SQL字符串到结构图的四步走
ZGLanguage的整体流程,我把它拆成四步:预处理、语法解析、血缘抽取、结构图渲染。这四步各自独立,中间用标准数据接口衔接,方便单独替换实现。比如语法解析这块,你今天想支持MySQL,明天想支持PostgreSQL,只要把方言适配层写好,后面的血缘抽取和渲染逻辑完全不用动。
第一步预处理做两件事:一是把SQL里的注释、空白、字符串常量归一化,避免这些噪声干扰后续的位置追踪;二是做方言改写,比如把MySQL的UPDATE ... JOIN改写成标准的UPDATE ... FROM形式,统一成同一种AST结构。第二步语法解析,ZGLanguage内部实现了完整的SQL词法分析和语法分析,产出带位置信息的语法树。第三步血缘抽取,遍历AST,识别目标表、SET子句、FROM/JOIN表、WHERE关联条件,生成血缘关系集合。第四步结构图渲染,把血缘关系集合转成图数据结构,再排版成SVG或者JSON输出。
这四步听起来简单,但每一步都有细节。预处理阶段最容易被忽视,我遇到过一条SQL里包含了注释掉的旧逻辑,注释里正好也有UPDATE关键字,如果不预处理直接解析,语法树就会多出一堆幽灵节点。所以我专门写了一个剔除注释和字符串内容的净化器,同时保留原始位置映射,保证报错和血缘来源都能溯源到原始SQL。
2.2 为什么我坚决不用正则去解析UPDATE
可能有人会问:UPDATE语句也不复杂,写几个正则、把表名和列名抠出来不就行了?我最初的原型就是这么干的,用了不到200行正则,确实能应付最简单的单表更新。但一旦SQL变成真实业务里的样子,正则方案就会全线崩溃。比如UPDATE t SET c = CASE WHEN a > 1 THEN (SELECT max(b) FROM s) ELSE 0 END,你根本没法用一个正则稳定地匹配到嵌套子查询里的表名,更别说区分这层括号属于函数还是子查询。
正则的另一个致命伤是没有“作用域”概念。它只能按字符模式去匹配,不知道当前这段文本到底在第几层嵌套里,也不知道一个列名究竟属于哪张表。碰到两个子查询里都有id字段,正则只能靠运气去猜,这在血缘这种讲究准确性的场景里完全不可接受。所以我后来果断换成真正的语法解析器,虽然代码量上去了,但语法树天然带层级结构,每个节点都知道自己是谁的子节点,列引用也能跟着作用域走,准确率完全不是一个量级。
这里多说一句,ZGLanguage的解析器不是重复造轮子。我参考了ANTLR官方SQL语法文件的组织方式,把关键字、表达式、子查询、JOIN条件做了模块化拆分,再针对UPDATE做了专门的AST节点类型。这样既能复用成熟的语法设计经验,又能按自己的需求扩展。
2.3 结构图选型:为什么用JSON+SVG而不是现成图数据库
血缘抽取完之后,接下来面临一个选择题:用什么来展示结构图?市面上有G6、vis.js、D3这些前端可视化库,也有现成的图数据库可以存储查询。但ZGLanguage的目标是“能嵌入到任何数据平台里”,所以我选择了一种更轻的组合——解析结果统一输出为结构化的JSON图数据,然后默认渲染成SVG。
JSON图数据的格式很简单,就是节点列表和边列表。节点可以标记为“表”或“字段”,边记录起点、终点和关系类型。这样的好处是,前端可以拿JSON直接画图,后端也可以把它落库做血缘检索,甚至能导入到Neo4j这类图数据库里做深度分析,我不需要绑定任何一家可视化厂商。SVG这边的渲染,我用的是自己写的一个极简布局器:先按血缘方向做分层,把目标表放在左侧,源表放在右侧,子查询节点放中间,再按字段数量分配纵向位置。
有人会问,为什么不用Mermaid或者DOT格式输出?最初我也考虑过,DOT的布局算法确实成熟,但它的输出文件依赖Graphviz,很多环境没法直接渲染。Mermaid则更适合文档场景,在复杂血缘关系下,节点的排布和交互能力都不够灵活。JSON+SVG虽然朴素的,但好在可控,我可以精确定义每一个节点的坐标和颜色,也能方便后续扩展成点击下钻的交互图。
3. 核心实现:UPDATE SQL 血缘解析
3.1 先梳理 UPDATE 语句的几种语法形态
写解析代码之前,我花了很长时间整理UPDATE的可能形态。这不是闲得慌,而是因为每一种形态对应着不同的AST遍历路径。我整理了四类最常见的情况,这四类基本覆盖了日常业务SQL的95%。
第一类是普通单表更新,UPDATE t SET c1 = expr, c2 = expr WHERE ...,这种最简单,目标表就一个,SET表达式里的列如果没带别名,默认都属于目标表。第二类是多表关联更新,典型的是MySQL的UPDATE t1 JOIN t2 ON t1.id=t2.id SET t1.c = t2.c或者SQL Server的UPDATE t1 SET t1.c = t2.c FROM t1 INNER JOIN t2 ON ...。这类解析的要点是,SET左侧列一定属于目标表,右侧列需要通过别名定位到具体是哪张表。第三类是子查询更新,SET右侧是一个标量子查询,比如SET c = (SELECT max(x) FROM s WHERE s.id = t.id),这里目标列c的血缘来源是s.x,同时子查询的关联条件s.id = t.id会产生一条表级关联。第四类是CTE更新,WITH tmp AS (SELECT ...) UPDATE t SET c = (SELECT ... FROM tmp ...),CTE相当于一个中间虚拟表,血缘穿透到CTE内部的源表。
每一类形态,我在代码里都用一个专门的visitor类去处理。这样做的收益很直接:新增一种方言语法时,不需要改其他类型的代码,只要注册一个新的visitor就行。
3.2 解析步骤一:先锁定目标表
整个UPDATE血缘解析,第一步永远是锁定目标表,这一点必须在所有其他逻辑之前完成。原因很好理解,SET子句左侧的列,以及WHERE里可能出现的关联列,都要以“目标表是谁”为基准来判定归属。
在AST层面,目标表就是UPDATE关键字后面紧跟的表表达式节点。这个节点可能是简单的表名,也可能是带schema的表名,还可能带别名。我在抽取时会把“真实表名”和“别名”分开存,真实表名用于血缘展示,别名用于在SET表达式里解析列归属。比如UPDATE ods.order_detail AS o SET o.status = ...,这里真实表名是ods.order_detail,别名是o,后续看到o.status就知道它属于目标表。
还有个容易出问题的点:目标表在有些方言里可以带LATERAL或者ONLY这类修饰符,虽然业务上很少用,但解析器不能因此报错。我在设计语法时把这些修饰符都定义成可选节点,遇到就跳过,不影响目标表定位,同时保留修饰符信息,方便以后做语法层面的精确还原。
3.3 解析步骤二:SET子句的左右侧拆分
SET子句是UPDATE血缘的核心战场,因为字段级血缘几乎全在这里产生。我的做法是把每个SET赋值项拆成“左侧目标列”和“右侧来源表达式”,然后分别处理。左侧的解析相对简单,它必须指向目标表的一个真实列,我会把AST里的列名节点转成一个目标字段对象,例如(schema.table, column)。右侧就要复杂得多了,它可能是一个列引用、一个函数调用、一个CASE WHEN、一个子查询,甚至是一整段算术表达式。
对于右侧表达式,我设计了一个递归的“列引用收集器”。它会深度遍历表达式子树,收集所有叶子列引用节点,同时记录每个节点所在的位置和所属的括号层级。这里有一个关键判断:哪些列应该算血缘来源,哪些不算。我的规则是,表达式中被“读取”的列都算来源,而常量、函数参数里的字面量不算。比如SET amount = price * 1.2,来源就是price;SET amount = 100,来源为空,这条更新属于“写入固定值”,血缘图上只会有一个目标列节点。
子查询作为右侧表达式时,不能简单把子查询里的所有列都拉平作为来源,因为子查询内部可能有自关联、可能有聚合,需要把子查询当成一个独立的“查询作用域”来递归解析。递归完之后,子查询的结果列只会映射到最外层的目标列上,子查询内部的中间列不直接暴露给外层。这样画出来的血缘才符合逻辑:外层列依赖的是子查询的“输出列”,而不是子查询的全部内部细节。
3.4 解析步骤三:WHERE 和 FROM/JOIN 的条件血缘
WHERE和FROM/JOIN的处理,很容易被认为“就是找找表名”,其实这里最容易产出脏血缘。我的核心策略是区分“关联条件”和“过滤条件”:关联条件会产生表级血缘,过滤条件不产生。判断依据是条件表达式里是否涉及两个不同表的列,比如t.id = s.id这种等值连接就是关联条件;而t.status = 'active'这种只涉及单表列和常量的属于过滤条件,它不影响数据来源。
在标准UPDATE里,FROM/JOIN子句里的表都是“来源表”,我会为每个来源表创建一条表级血缘边,方向是从来源表指向目标表。但如果FROM里带子查询,那就不是简单的表级血缘了。比如UPDATE t SET c = x.c FROM (SELECT id, c FROM s) x WHERE t.id = x.id,这时来源表实际上是s,x只是中间别名,血缘边应该从s直接指向t,同时字段来源是s.c。
再回到WHERE条件,如果它是一个复合表达式,比如WHERE a.id = b.id AND b.type = 1,我会把等值关联部分抽出来生成关联边,同时保留过滤条件在AST里,但不生成血缘边。这个设计让我在后面的实际验证中,把血缘图的准确率拉高了很多,因为以前很多工具会把所有出现在WHERE里的表都画成“上游表”,导致血缘图膨胀得没法看。
3.5 血缘关系模型:字段级和表级分开存储
解析过程中产生的血缘关系,我在ZGLanguage里用统一的数据模型来承载,核心就是两个对象:节点和边。节点分为表节点和字段节点,字段节点必须挂在某个表节点下面。边则分为字段血缘边和表血缘边,字段血缘边的方向是从源字段指向目标字段,表血缘边是从源表指向目标表。
这里我踩过一个设计坑:一开始只存字段血缘,结果发现有些表级关系里说不清楚具体字段对应,比如UPDATE t SET c1 = s.a, c2 = s.b,如果只存两条字段边,下游查“t依赖哪张表”时还得去扫字段边,性能很差。后来我在生成字段血缘的同时,会自动向上汇总出表级血缘,并把字段血缘作为表级血缘的明细挂在边上。这样既支持字段级下钻,又不影响表级概览。
这个模型还考虑了“血缘类型”标记。ZGLanguage用edge_type字段区分这条边是来自表达式计算、关联条件、子查询还是JOIN,方便后续在结构图里用不同样式渲染。比如关联条件的边用虚线,表达式来源的边用实线,子查询来源的边用点划线。可视化之后,读者一眼就能识别出“这条更新是直接取值,还是经过复杂计算”。
3.6 结构图生成:从血缘集合到坐标布局
血缘数据生成之后,结构图渲染是最后一步。ZGLanguage没有用复杂的力导向图算法,而是选择了一个更适合血缘阅读的分层布局算法。我把节点分成三层:最左层是目标表及其字段节点,中间层是子查询或中间表节点,最右层是最终来源表节点。这个顺序和数据流向一致,人眼看起来就是“数据从右往左流入目标表”。
每个表节点下方,我会把涉及血缘的字段列出来,用一个小矩形表示。字段之间有纵向的边连接,避免线与线交叉。当字段很多时,我做了简单的排序和分行处理,尽量让边只跟最近邻的节点相连。最终输出SVG时,目标表用深色边框,来源表用浅色,中间节点用虚线框,整体视觉重点落在目标表上。
说实话,这个布局算法做不到专业图数据库那么美的效果,但对于血缘分析场景已经足够。实际用下来,最让我满意的不是它多好看,而是它能迅速暴露解析问题:如果某个来源字段落到了错误的层级,SVG里的边会明显绕路,我马上就能知道是解析器哪一步出了问题。
4. 实操过程与效果展示
4.1 运行环境与准备
ZGLanguage的解析核心用Python实现,原因是Python做AST遍历和原型验证最顺手,而且后续要接pandas做血缘校验也很方便。运行时依赖很少,核心只依赖一个Python标准库,可选的SVG渲染模块需要安装一个轻量库。我把整体代码放在一个zgl_parse.py脚本里,命令行调用,适合嵌入到定时任务或数据平台API里。
环境准备就三步:先装Python 3.10以上版本,然后把项目代码克隆到本地,最后安装可选的渲染依赖。我没有依赖重型的图数据库,因为对于“解析并显示结构图”这个目标,单机脚本已经足够。如果你的SQL量特别大,后续可以把血缘结果导出成JSON,分发给后端服务去做存储和查询。
4.2 拿来练手的几条真实风格UPDATE语句
为了验证效果,我准备了一组覆盖不同语法的测试SQL。第一条是最常见的单表更新,看看基础能力。第二条是MySQL风格的JOIN UPDATE,验证方言改写。第三条是带标量子查询的更新,这是血缘最容易断链的场景。第四条是CTE加CASE WHEN的复杂更新,用来压测字段解析能力。
-- 样例1:单表更新 UPDATE ods.order_daily SET status = 'finished', amount = amount * 1.1 WHERE order_date = '2025-01-01'; -- 样例2:MySQL JOIN UPDATE UPDATE ods.order_daily AS o INNER JOIN dwd.dim_shop AS s ON o.shop_id = s.shop_id SET o.shop_name = s.shop_name, o.region_id = s.region_id WHERE s.is_valid = 1; -- 样例3:标量子查询更新 UPDATE dwd.product_snapshot SET current_price = ( SELECT max(price) FROM dwd.price_history h WHERE h.product_id = product_snapshot.product_id AND h.is_active = 1 ) WHERE snapshot_date = current_date; -- 样例4:CTE + CASE WHEN WITH valid_shop AS ( SELECT shop_id, shop_name, level FROM dwd.dim_shop WHERE is_valid = 1 ) UPDATE ods.shop_agg AS sa SET level_desc = CASE WHEN vs.level = 1 THEN '高' ELSE '普通' END, update_time = current_timestamp FROM valid_shop vs WHERE sa.shop_id = vs.shop_id;这四条语句覆盖了我前面讲的绝大部分难点:多表关联、别名、子查询、CTE、表达式函数、条件分支。把它们跑通,基本上就可以应对生产环境的主流UPDATE了。
4.3 执行解析并查看结构图JSON
我在项目里封装了一个最简单的代码入口,只要传入SQL文本,就能返回图数据结构。核心调用方式如下:
from zgl_parse import parse_update_lineage sql_text = open("update_case2.sql", encoding="utf-8").read() result = parse_update_lineage(sql_text) result.save_json("case2_graph.json") result.render_svg("case2_graph.svg")执行完之后,生成的JSON文件长这样,我贴一部分关键节点和边:
{ "nodes": [ {"id": "t_ods_order_daily", "type": "table", "label": "ods.order_daily"}, {"id": "f_ods_order_daily_shop_name", "type": "field", "label": "shop_name", "parent": "t_ods_order_daily"}, {"id": "f_ods_order_daily_region_id", "type": "field", "label": "region_id", "parent": "t_ods_order_daily"}, {"id": "t_dwd_dim_shop", "type": "table", "label": "dwd.dim_shop"}, {"id": "f_dwd_dim_shop_shop_name", "type": "field", "label": "shop_name", "parent": "t_dwd_dim_shop"}, {"id": "f_dwd_dim_shop_region_id", "type": "field", "label": "region_id", "parent": "t_dwd_dim_shop"} ], "edges": [ {"src": "f_dwd_dim_shop_shop_name", "dst": "f_ods_order_daily_shop_name", "edge_type": "expression"}, {"src": "f_dwd_dim_shop_region_id", "dst": "f_ods_order_daily_region_id", "edge_type": "expression"}, {"src": "t_dwd_dim_shop", "dst": "t_ods_order_daily", "edge_type": "table"}, {"src": "t_ods_order_daily", "dst": "f_ods_order_daily_shop_name", "edge_type": "field_parent"}, {"src": "t_dwd_dim_shop", "dst": "f_dwd_dim_shop_shop_name", "edge_type": "field_parent"} ] }可以看到,字段级血缘dwd.dim_shop.shop_name -> ods.order_daily.shop_name被完整保留了,表级血缘也自动汇总成dwd.dim_shop -> ods.order_daily。这个JSON可以直接交给前端画图,也可以落库做后续查询。
4.4 结果校验:人工核对四张结构图
JSON再漂亮,也得验证对不对。我把四条SQL解析出来的结构图拿给团队里两位数仓同事看,让他们手工核对血缘关系。核对重点有三个:目标表是否准确、来源表是否有遗漏、字段对应关系是否正确。
核对结果比较理想。样例1里amount = amount * 1.1被识别为自引用,血缘图会出现一条从ods.order_daily.amount指向自身的边,这个自环是合理且必要的,它表达了“旧值参与新值计算”的逻辑,比某些工具直接把自环删掉要准确得多。样例2的JOIN UPDATE,两条字段血缘都正确指向了dwd.dim_shop,同时没有把s.is_valid = 1这个过滤条件误判成字段来源。样例3的标量子查询,血缘正确穿透到dwd.price_history.max(price),最外层目标列current_price只跟这个子查询输出关联。样例4的CTE,血缘穿透到了dwd.dim_shop.level,而CASE WHEN里的常量'高'、'普通'没有产生虚假的血缘边。
5. 踩坑记录与排查技巧
5.1 子查询作用域处理不到位,导致列归属飘了
我第一次跑通样例3时,发现h.product_id = product_snapshot.product_id里的关联列被错误地识别成了目标表的来源。排查了很久,问题出在子查询作用域上:AST遍历时,我在进入子查询节点之前没有把外层作用域压栈,导致解析器认为product_snapshot.product_id是子查询内部的列引用。后来我加了一个作用域栈,每进入一个子查询就压入一层,遇到列名时从最内层往外找,找到的第一个所属表作为列归属。这个改动是ZGLanguage准确率的转折点,改完之后很多之前对不上的血缘都对了。
5.2 同名字段在多表JOIN里乱认亲
多表JOIN时最头疼的问题就是同名列。比如样例2里ods.order_daily和dwd.dim_shop都有shop_id,WHERE里的o.shop_id = s.shop_id因为带了别名还好说,但如果业务SQL写得不规范,直接写shop_id = shop_id,解析器就会懵。我的兜底策略是:能通过别名定位的优先用别名,没有别名的,先用目标表字段集去匹配,匹配不上再按“最后出现的表”找。这个策略不是百分百准确,所以我还在JSON里加了一个confidence字段,遇到这种歧义解析时标记为低置信度,让使用方知道这里需要人工确认。
5.3 方言预处理要放在最前面
MySQL的JOIN UPDATE和SQL Server的FROM UPDATE,在语法树上的结构差异很大。为了让血缘抽取层只面对一种AST结构,我把方言改写放在了预处理阶段,而且用了一个很笨但很稳妥的办法:先写一小段规则,把MySQL的UPDATE t1 JOIN t2 ON ... SET ...改写成UPDATE t1 SET ... FROM t2 WHERE ...的等价形式,再进行标准解析。这个改写只做结构变换,不改业务语义。这样做的效果是,血缘抽取代码只认识一种UPDATE语法,方言差异全部被隔离在最外层,后续如果要支持Oracle的MERGE,也只需要再加一个改写规则。
5.4 大SQL带来的性能问题
生产环境的UPDATE语句经常是几百行甚至上千行,嵌套子查询七八层。最开始我的解析器碰到这种SQL会明显卡顿,后来用cProfile定位发现,性能瓶颈在子查询递归解析时重复遍历了相同的表节点。我的优化是给每个表节点加了一个解析缓存,相同的schema.table组合在一条SQL里只解析一次。同时,我把递归深度限制在默认10层,超过这个深度就告警并跳过深层血缘,避免解析器陷入无限循环。实测下来,一条800行的UPDATE SQL,解析加渲染能在2秒内完成,这个速度对血缘分析场景完全够用。
5.5 常见问题速查表
我把实际使用中大家问得最多、也最容易被忽略的问题整理成了一个表,方便你遇到类似情况时快速对照排查。
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 目标表识别成来源表 | UPDATE后跟了WITH查询,目标表定位逻辑没找到正确节点 | 确认预处理阶段是否处理了CTE,目标表定位必须在WITH之后的UPDATE关键字处 |
| SET右侧的列血缘丢失 | 表达式里有函数嵌套或CASE WHEN,列引用收集器没做深度递归 | 检查收集器是否对函数实参和分支子句都进行了深度遍历 |
| 出现幽灵来源表 | WHERE里的过滤条件被当成了关联条件 | 检查条件分组逻辑,过滤条件不生成血缘边 |
| 同名列归属错误 | 多表JOIN没有别名,解析器按兜底策略猜错 | 查看JSON里的confidence字段,低置信度的人工复核 |
| UPDATE JOIN语法解析报错 | 方言预处理未生效,解析器碰到了不认识的语法 | 确认预处理规则是否注册了该数据库方言 |
| 结构图边交叉严重 | 字段节点排序策略太简单,没有按连接顺序排序 | 调大纵向间距,或者调整字段节点排列顺序 |
5.6 实测下来最重要的一条经验
ZGLanguage从原型到能用,我最大的体会是:数据血缘解析的结果一定要可视化,而且要尽早可视化。很多人做解析器,喜欢先写一堆单元测试来验证输出,但单元测试只能覆盖你想到的情况,而真实SQL的怪异写法永远超出你的预料。把每次解析结果直接渲染成结构图,然后肉眼扫一遍,那些乱的边、漂移的节点、缺失的表,马上就能暴露出来,这比任何日志都直观。
我现在的工作流是:拿到一批新SQL,先批量跑一遍ZGLanguage,把所有结构图导成SVG,然后按图快速浏览,凡是看起来“不对劲”的图,再去翻对应SQL和AST日志。靠着这个流程,我在一个下午内修复了七八个解析边界问题,效率远高于对着控制台输出调参数。
另外还有一个非常实用的小技巧,处理UPDATE之前先把SQL做一次格式化,给所有表和列补上明确的别名。很多业务SQL本身写得不规范,解析器再强也架不住歧义满天飞,格式化加补别名之后,准确率会直线上升。ZGLanguage在预处理阶段已经内置了一个轻量级的格式化函数,我建议你在接生产SQL之前先统一走一遍,你会发现踩坑数量至少减少一半。
ZGLanguage现在只把UPDATE这条主干打通了,但它的架构决定了后续扩展INSERT和MERGE并不难,因为血缘模型、结构图渲染、方言预处理这几层都是通用能力。我个人接下来的计划是加入MERGE语句的支持,让ZGLanguage能覆盖数仓里更完整的写入类SQL场景,然后把结构图升级成可交互的网页版本。如果你也在折腾SQL血缘解析,建议从UPDATE先下手,它是最难啃但也最能验证解析器能力的试金石。