列级血缘图画出来之后,我们决定不直接给用户看
全量血缘留给机器,能回答问题的图留给人
列级血缘解析刚跑通的那几天,我们的成就感是满格的。一段 SQL 粘进去,每个字段从哪来、经过谁、变成了什么,全画在一张图上:表卡片挂着列,列与列之间连线,悬停在线上还能看到口径表达式。做工具的人看到自家解析器吐出这种图,心情跟看孩子学会走路差不多。
然后第一个真实用户打开了图。看了一分钟,问了一个问题:哪个是我们的源头表?他不算苛求的用户,试着拖了拖,缩放了缩放,在连线堆里找了一会儿,关掉了页面。
那一刻图上是这样的:一段不长的建表脚本,解析出来几十个节点、几十条连线。表卡片下面挂着列,列中间夹着解析器为 CTE、JOIN、常量生成的中间节点,真表、视图、临时区混在一起。语句一多,节点数翻着倍地涨。对我们来说这是解析能力的成果,对用户来说是一张蜘蛛网。他不想欣赏全部,他只想知道一件事,而那张图没有回答他。
我们最初把这次反馈当成审美问题,琢磨过要不要把配色和布局再抛光一遍,没用。问题不在好不好看,在它没在回答。把整个事情捋了一遍,我们发现是两个问题被搅在了一起。解析层回答的是:这条 SQL 产生了哪些血缘。它必须全,漏一条就是事故,几千条测试用例日日夜夜钉着它。用户要回答的是:这张表的下游有哪些,这个字段最上游是谁,这次改动会波及什么。他要的不是全量,是跟他手里那件事相关的那一片,剩下的最好先别烦他。
麻烦在于,第二类问题天天换花样。今天是找源头,明天是看影响面,后天是新同事问这张表的数据到底准不准。问题没法枚举,就没法靠预制几张汇总视图打发,你得让图能听问题。这也解释了一件事:为什么演示时人人点头,交付出去没人打开。演示的人自带问题清单,真实用户带着的是他们自己的问题。
全量列级血缘是给机器算的,不是给人看的。这个道理现在说出来一句顶一句,当时是拿一张没人看得懂的图换来的。
修法最后落在一条原则上:解析层一行代码不动,在它上面加一层只读的查询视图。
不动解析层,不是护短。它是拿测试喂大的,每种方言、每种语法都有快照钉着,动一个字,几十条用例跟着变红。它的唯一指标是全对,别的什么都别掺进去。查询层干的活,是把这些全对的结果按需整理:你问上游,我给你上游;你要表级,我把列折叠成表;你想跳过中间过程,我把流经的血缘接好给你。
为什么是只读?因为视图层不生产事实,它只负责事实的排版。血缘一旦允许在浏览的时候被顺手修改,两个层迟早各说各话,没人再敢信图。这层计算还有一个要紧的安排:只放服务端,前端只管发起和画图。客户是拿 API 集成的,计算放进浏览器,别人就拿不到这套能力;两端各写一套遍历,半年后就是两个语义。
后来的事实验证了这套分层值钱:视图层两天里反转过好几轮设计,解析层一次都没被牵连。这话反过来讲也成立,正因为动不了也不必动解析层,视图层改起来毫无心理负担。架构的分界线画在哪里,决定的是以后每次改动的心跳速度。
最先撞上的是方向。
血缘图上的连线分两种性格。一种性格直,列到列,数据真的从这头流到那头;另一种性格弯,只负责把条件里同时出现的两列牵到一起。JOIN 条件就是后一种,它像媒人,不像水管。JOIN ON a.customer_id = c.id,解析出来的这条边只说明这两列有关联,数据从谁流向谁,条件本身没说。早期版本没把这层分开,有人查一列的上游,返回里出现了它的下游。这种错误还特别安静:接口照常返回,图照常画,只是答案悄悄不对。血缘工具给的是结论,用户拿着结论去做决策,错了都不会有响声。顺着这个 bug 查下去,发现的不是某处代码写错,而是语义从来没立过规矩。
后来立的规矩很硬:JOIN、WHERE 过滤、改名这类条件边只表示"有关联",不承载流向;直接映射、转换、聚合、合并这些边才算数据流。查上游查下游,只沿数据流走;想把条件关联一起看,那是另一个镜头,界面上明说。顺带说一句,JOIN 本身的数据流也有讲究:两张表的字段会先流进解析器为这次 JOIN 造的一个中间区,再流向结果。查上游时中间区显不显示,归过滤管,方向只管流的顺序。条件边和数据流边各管各的,图才说得清。规矩立完,这类问题再没回来过。
方向理顺之后是粒度。
列级血缘的价值在列,难看的原因也在列。一张表几十个字段,两张表一关联,图就铺开了,用户还没找到关心的问题,先迷了路。而多数时候,用户先想知道的是表和表之间是什么关系,看到某张表可疑,再点进去看列。所以视图层给了一个开关:列折叠成表。折叠不是删掉,是收起来,你看到的线少了,血缘的事实一条没丢。
折叠之后,几十条连线缩成两三条,表和表的关系一眼见底。看到哪张卡片可疑再展开,列级细节原样在那里等着。首屏默认也给简版,中间过程收缩,只留数据链;想考古的人自己展开全图,两不耽误。先粗后细这四个字,地图产品十年前就教过所有互联网用户,血缘图没理由不学。
深度也归这个镜头管。图太深就分层看,一次看一跳、看 N 跳,或者放开看全部。放开的时候有个小设计:超过上限,明确标记图被截断了。宁可诚实地讲我只给你看了前几层,也不让你以为看到的就是全部。误导性的完整,比诚实的残缺危险得多。
真正费心思的是过滤。
需求听着简单:把中间过程藏掉。要面对的中间角色还不少:CTE、子查询、解析器为 JOIN 和合并造的中间区、常量、星号展开出来的星节点,全是解析的功臣、浏览的负担。举个最小的例子:WITH x AS (SELECT id, amount FROM orders) SELECT id FROM x,解析出来的血缘链是三跳,orders 的列流向中间视图 x,x 再流向查询结果。中间那一跳是解析的必需品,却是浏览的噪音,用户想直接看的是 orders 和结果的关系。
第一版我们真当它是删节点,删完问题一个接一个。这个直觉为什么错,值得展开讲:列级血缘的图不是画出来的,是算出来的,每条边都是解析器对着 SQL 逐个表达式求出来的事实。你从图上删掉的不是一个框,是推理链上的一环,后面所有依赖这一环的结论都会跟着塌。
藏掉一个视图,它名下的列怎么办?自然跟着消失,这叫层联。可上游流进它、下游从它流出的血缘怎么办?得接上。视图层把这些边重新连起来,入口直连出口,新的边会记住自己是由几条边缩出来的、走过哪些类型的边。图变简洁了,每条线背后的信息没变薄。
有些血缘接不起来。一段血缘只流进被藏的区域,到不了任何保留的节点,它就跟着一起消失。它对你已经不可见,留在图上只是噪音。
链头链尾要豁免。最初的版本图省事,所有"中间"不分场合一律抽走,有用户取消子查询之后,图上啥也不剩:链的源头是真表,终点是查询输出,都不算中间,唯独中间那截没了,头尾悬在半空。后来加了一条豁免:血缘链的头和尾,真表、最终输出,永远保留,连列一起。不管怎么过滤,一条链从哪来、到哪去,必须始终看得见。
桥接还有一条边线:只接被藏的区间。路径一旦经过还留在图上的节点,就到它为止,绝不多画一条跨过它的捷径。否则图上看着是直达,实际中间隔着一层转换,哪天有人拿这条线去做影响判断,就是编了一个不存在的依赖进图。什么都不藏的时候,视图给出的边和原图是同一批对象,一个不多,一个不少。层联、桥接、豁免、禁捷径,四条规则合起来守住同一条底线:视图层可以藏事实,不能改事实,图上剩下的每一条线,都还经得起指着问一句,这是真的吗。
操作上,过滤是一个按类型分组的勾选面板:表、视图、临时表、CTE、子查询、常量、函数过程,每一行标着原图里这类节点的数量,数量为零的灰掉;表和视图锁住不许取消,表级视图的语义不会被误碰改坏。类型过滤也不用先选中谁,整张图直接换镜头。
还有一个顺手但常用的镜头:终点。
用户的原话通常是,我就想知道这列最上游是哪张表,或者这张表最终流进了哪张报表,中间的不关心。视图层给了两个动作:往上游一路走到头,拿到真源头;往下游一路走到头,拿到最终去向。这两个动作看着朴素,用起来频率意外地高。新同事接手一张表,第一个问题就是上游哪来的;做报表的人最怕下游忽然有人喊口径变了,提前查一次最终去向,心里就有底。一次点击,省掉一下午翻代码。
工具栏上给这两个动作留了两个专门的按钮:最上游来源、最终去向。选中一个点再按,图一路收缩到头,再点一次退出。入口后来还铺到了目录树,在任何一张表、任何一列上右键,同一套查询菜单跟着弹出,它不在当前图上也没关系,服务端在全图上算,起点会自己走进结果。
这些镜头拼在一起,是一条工具栏加一套右键菜单。拿一次真实使用走一遍:粘贴一条几十行的建表脚本,解析完先不急着看列,直接按表级,十来张卡片、几条关系线,主链一眼见底;右键业务主表选看上游,源头两张表浮出来,中间隔着几层加工;哪层可疑点开哪层,列级细节原样等着,悬停连线还能看口径;加工过程不想看了,勾掉 CTE 和子查询,上下游被桥接边接上,边上标着由几条边缩成;最后按一下最终去向,这条链一路流进哪张报表,几秒有了答案。全程没翻过一行代码。
难点在"头"的判定。图里有些节点天生没有下游,比如 JOIN 条件里的一列,它自己没有下游,但它所属的表还在向下游供数。这种节点看着是终点,其实是半路。判据最后落在容器上:看它所属的表还有没有往外流的血缘。半路的终点不算终点,接着往下找。追溯的粒度也跟着起点走:从列出发,到头的是列;从表出发,到头的是表。问什么粒度的问题,拿什么粒度的答案。这个判定在测试里翻过一次车,先写的测试红着跑了一遍,抓出一个粒度错位,修完才绿。
这些查询最后落成一个接口,示意一下请求的样子:
POST /api/v1/lineage/query
{
"analysis_id": "demo",
"view": {
"start": { "type": "container", "id": "public.orders" },
"direction": "upstream",
"depth": 3
}
}
start 里的 type 是 container 还是 column,意思是起点选的是表还是列。返回收缩后的节点和边,附上输入输出的规模统计。这栏数字是给自己人看的照妖镜:输入几十个节点,输出剩几个,缩没缩、缩了多少,摆在明面上,谁都不用猜。参数不合法,接口直接报错讲清楚错在哪,不猜你的意图。超深截断会带标记。计算全在服务端,前端只管发起和画图,界面上那几个按钮,方向、深度、折叠、类型,各自对应请求里的一个字段,人点的和程序发的走同一条路。
快也是这个层级的必修课,原则是不一次拉全图。沙箱里的一次分析规模有限,目录里的一张图却可能大到没法一次载入,那就先按数据源和表做前置裁剪,范围缩小了再算。同一张图上反复查方向和深度,不再整图重载,加载过的图留在内存里反复用。深度有顶,结果有界,超了就明说,这是这个层级所有功能共同的前提。
商业上有个顺带的安排:血缘解析收费,血缘查询免费。解析是引擎,查询是查看已有的结果。免费还有一层实际考虑:查询是放大器,血缘被看得越多、用得越深,工具才算真的进了团队的工作流;被反复查看的血缘是活的,锁起来的血缘只是存档。图都画出来了,看图还要再收一次钱,这生意就做得太小了。
这层视图做下来,比加功能更值钱的反而是两次放弃。
一次是路径追踪。规划都做好了:选两个节点,给你最短路径,或者全部路径。选点、菜单、高亮都做出来了,拿去试用,反馈很一致:用起来很奇怪,想不清楚它帮用户解决了什么问题。复盘下来,单图上点选高亮已经能看"经过它的一切",两点路径的增量价值太小;真正用得上它的场景在几千张表的目录图上,那是另一个阶段的事。代码一行没上线,规划整体撤销,改动原样还原。功能清单少了一项,产品清爽了一圈。
那次撤销最值钱的结论是:规划的沉没成本不是理由。规划做了三天,试用十分钟就推翻它,很多人下不去手,于是产品里多一个谁也不用的按钮。判据其实简单:用户用不用,比规划完不完整重要得多。
另一次是反转,起因是一张明显不对的图。有用户把表级视图的返回贴过来,节点里混着函数、过程、变量、触发器,一眼就能看出不对。顺着查下去,落成两个教训。一个教训是默认值要硬:表级视图就该只有表和视图,其余一律不出现,松垮的默认对新用户友好,对认真的人是坑。另一个教训藏在传参方向上。过滤参数原来传"要隐藏哪些类型",上线后意识到一个隐患:以后每新增一种节点类型,它都会悄悄漏进老请求的显示范围,"默认全显示"这个语义跟着类型清单自动膨胀。后来把传参整个反过来,传"要显示哪些"。白名单的世界很安静:你没点名的东西,永远不该出现。
这套分层的顺序别反:先有算得全的解析,再谈讲得清楚的视图。底下没有事实,浏览层做得再顺滑,也只是好看的空壳。
回头看,两层各干各的活:解析层管血缘算得全不全,视图层管血缘讲得清不清楚。算全靠纪律,讲清楚靠克制:不动底下的事实,不编上面不存在的依赖,给不了完整就明说截断。
还是那张图,还是那些节点,变的只是它开始回答问题了。用户进来第一眼能拿到答案,第二眼才轮到看全部。
最后问问正在做数据平台的你:打开自家的血缘图,用户第一眼能找到那张源头表吗?如果找不到,先别怪用户不会用,看看你的图在回答谁的问题。
▎ Esther 是一个企业级 SQL 数据血缘分析平台:28 种方言、列级血缘、元数据管理、私有化部署 。数据安全合规的隐性账——敏感数据流到哪、怎么流的——这条路我们在趟,你不必自己从零搭。如果你的团队正在做等保/个保法合规的数据安全治理,欢迎聊聊。
▎ 系列文章和配套 SQL 用例都放在仓库里了,能直接跑:github.com/estherdata2026