
做数据治理的朋友应该都有同感数据血缘解析这活儿看起来是“把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.cSQL 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 结构图选型为什么用JSONSVG而不是现成图数据库血缘抽取完之后接下来面临一个选择题用什么来展示结构图市面上有G6、vis.js、D3这些前端可视化库也有现成的图数据库可以存储查询。但ZGLanguage的目标是“能嵌入到任何数据平台里”所以我选择了一种更轻的组合——解析结果统一输出为结构化的JSON图数据然后默认渲染成SVG。JSON图数据的格式很简单就是节点列表和边列表。节点可以标记为“表”或“字段”边记录起点、终点和关系类型。这样的好处是前端可以拿JSON直接画图后端也可以把它落库做血缘检索甚至能导入到Neo4j这类图数据库里做深度分析我不需要绑定任何一家可视化厂商。SVG这边的渲染我用的是自己写的一个极简布局器先按血缘方向做分层把目标表放在左侧源表放在右侧子查询节点放中间再按字段数量分配纵向位置。有人会问为什么不用Mermaid或者DOT格式输出最初我也考虑过DOT的布局算法确实成熟但它的输出文件依赖Graphviz很多环境没法直接渲染。Mermaid则更适合文档场景在复杂血缘关系下节点的排布和交互能力都不够灵活。JSONSVG虽然朴素的但好在可控我可以精确定义每一个节点的坐标和颜色也能方便后续扩展成点击下钻的交互图。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.idt2.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来源就是priceSET 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这时来源表实际上是sx只是中间别名血缘边应该从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; -- 样例2MySQL 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; -- 样例4CTE 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, encodingutf-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_idWHERE里的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先下手它是最难啃但也最能验证解析器能力的试金石。