存储过程血缘解析:过程体内SQL的抽取方法与实战指南
存储过程里的血缘:过程体里的 SQL 怎么挖出来
做数据治理和元数据管理的朋友,大概率都遇到过这样一个尴尬场景:数据地图上表与表之间的血缘关系清清楚楚,结果业务方一问“这个字段到底怎么算出来的”,你顺着链路往下追,追到一个存储过程就断了。不是不想往下挖,是存储过程这个东西实在太“黑盒”——一堆逻辑塞在一个过程体里,外人根本不知道里面执行了哪些SQL、这些SQL又做了什么转换,血缘到这里就成了一条断头路。
这个问题的本质,其实是我们平时做的血缘解析,大多只覆盖了“显式SQL”:你写一条 INSERT INTO table_a SELECT ... FROM table_b,解析器一看就知道 table_b 流向 table_a。但存储过程把SQL包在了过程体里,里面有变量声明、游标循环、条件判断、动态拼接,这些都会干扰解析。再加上不同数据库(MySQL、Oracle、SQL Server)存储过程的语法差异,同一个逻辑在不同库里写法能差出十万八千里,所以真正要把“过程体里的SQL挖出来”,靠单一解析器硬啃基本行不通。
我在这块儿踩了不少坑,也沉淀了一些自己的方法和工具链。这篇就专门讲讲,存储过程里的血缘到底怎么挖,过程体里的SQL怎么抽出来、怎么解析成可靠的字段级血缘。不管你是做数据平台、数据治理工具,还是单纯想搞清楚自家数仓里某张报表的加工逻辑,这篇文章的思路都适用。
1. 为什么存储过程里的血缘这么难挖
在动手写解析代码之前,先得理解清楚一个问题:存储过程这玩意儿为什么让所有血缘工具都头疼。只有把难点拆透了,你才能知道后续每一步操作是为了解决什么问题。
1.1 过程体不是单条SQL,而是SQL的“综合体”
普通的一次查询,是一句完整的 SELECT,结构清晰、边界明确。但存储过程的过程体里面,至少包含这几类东西:
- 变量声明区:
DECLARE或者SET @var = ...,这些不是血缘对象,但会干扰解析 - 流程控制语句:
IF...ELSE、CASE、LOOP、WHILE、游标循环 - 数据操作语句:
INSERT、UPDATE、DELETE、MERGE,这才是血缘的主要来源 - 动态SQL:拼接出来的字符串,然后通过
EXECUTE IMMEDIATE或PREPARE执行,文本上完全看不出来它最终会访问哪些表 - 临时表操作:
CREATE TEMPORARY TABLE、CREATE TABLE ... AS SELECT,中间结果也会参与血缘传播
举个例子,Oracle里很常见的一个存储过程片段:
这段代码里,orders 表经过两次加工最终写入 daily_report。单纯的正则匹配可以找到 INSERT INTO daily_report 和 SELECT ... FROM tmp_sales,但要完整串联出一条“orders → tmp_sales → daily_report”的完整血缘链,你就得把临时表这个中间变量也纳入追踪。这已经超出了“正则抽SQL”的范畴,是需要状态管理的解析活儿。
1.2 “显式SQL”解析和“过程体内SQL”解析差了十万八千里
主流血缘工具(比如 OpenMetadata、Atlas 的SQL解析器)处理普通SQL的能力很强,你扔给它一条 SELECT,它能给你吐出精确到字段级别的血缘。但一旦进入存储过程,它们的表现往往很拉胯,原因有三。
第一,存储过程的语法不是标准SQL。 每个数据库都有自己的一套方言。MySQL 里是用 BEGIN ... END 包一个复合语句,Oracle 里是 CREATE OR REPLACE PROCEDURE ... AS ... BEGIN ... END,SQL Server 则是 CREATE PROCEDURE ... AS BEGIN ... END。解析器如果不认识这些外层结构,就根本进不到过程体里去。
第二,存储过程里面有“状态”。 临时表、变量、游标、异常处理,这些不是静态文本能表达的。比如同一个临时表 tmp_sales,在过程体外是一个名字,在过程体内可能被连续写了好几次 DROP 和 CREATE,你要判断血缘走的是哪一次生成、哪一次消费,就必须模拟这个“赋值-使用”的状态流转。
第三,动态SQL把血缘藏起来了。 这是最脏的一类。很多老系统为了灵活,喜欢拼字符串来执行SQL,表名是变量拼进去的。这种情况下,静态解析只能看到一块拼接字符串的模板,完全看不到真实访问的表。你要么做Hive/Spark引擎级别的血缘解析,要么就在SQL提交的入口做运行时捕获,否则动态SQL就是血缘的“黑洞”。
1.3 主流的商业工具成本太高,自己动手反而更可控
市面上其实已经有商业化的血缘分析工具,能做到自动解析存储过程。但说实话,很多企业内部的数据环境是混乱的,存储过程上千个、嵌套十几层、还有一堆历史遗留的“祖传代码”,商业工具要配置好也是一堆活。而且商业工具通常是个黑盒,出了问题你不知道它为什么对、为什么错,想定制也困难。
我觉得对于很多团队来说,自己写一套“存储过程血缘解析器”是更实际的选择。不一定非得做成产品级别,只要能把你最关心的一批核心存储过程解析出来,把关键链路的血缘跑通,就已经价值很大了。而且自己写了以后,你对自己数据加工逻辑的理解会比看文档深刻得多。
2. 解析前必须做的事:先把存储过程“剥洋葱”
真正动手写解析代码之前,还有一步特别重要的前置工作:把存储过程从代码文件或数据库系统表里完整捞出来,并做好清洗和归类。别小看这一步,很多解析失败,问题根源其实不在解析器,而在“喂给解析器的料”本身就脏得不行。
2.1 从数据库里提取存储过程源码
不同数据库存储过程源码的获取方式不一样,一开头就要按库分开来做。
- MySQL:直接查询
information_schema.ROUTINES表,或者用SHOW CREATE PROCEDURE,可以拿到完整的建过程语句 - Oracle:比较常用的是查询
ALL_SOURCE、DBA_SOURCE视图,它是按行存的,需要做行拼接才能还原成完整文本 - SQL Server:查询
sys.sql_modules视图的definition字段,通常是一个NVARCHAR(MAX)的完整文本
这一步看起来简单,实际有个大坑:Oracle 的 ALL_SOURCE 分页存文本,每行前面带行号,取出来之后要按 TYPE、OWNER、NAME 排序后拼接,否则会乱。而且存储过程里如果带了中文注释,字符集衔接不好就会乱码,后续正则匹配全废。
2.2 格式标准化:注释、字符串、换行,一样都不能留
拿到了原始源码之后,先别急着解析。过程体里通常会混着注释和字符串字面量,这些对血缘解析完全没意义,但会严重干扰正则和关键词匹配。举个例子,代码里注释写着:
如果你不把注释先清掉,你那套匹配 INSERT、SELECT 的正则逻辑很容易被注释文本干扰出虚警。
处理办法也很直接:先写一个“清洗器”,按顺序做这么几件事:
- 删掉
--开头的单行注释和/* ... */的多行注释 - 把字符串字面量(用单引号包裹的)暂时替换成占位符,比如
__STR_1__,避免内部的关键词被误匹配 - 把
\r\n统一换成\n,保证后续按行分析时行号不错乱 - 把多个连续空格压缩成单个空格,减少正则回溯的开销
清洗完之后的文本,才算是一个“纯净的过程体”。这一步做完,后面解析的成功率能提升一大截。
2.3 从全局视角识别过程体的子程序嵌套
还有一个容易忽略的地方:存储过程并不是只能平铺一层逻辑,它可以嵌套调用其他存储过程,或者内部包含嵌套的 BEGIN...END 块。比如一个主过程 p_main 里调用了 p_step1、p_step2,那最后生成的完整血缘,应该是 p_main 内部的SQL产生的那些血缘,加上子过程产生的血缘。
所以做解析前,最好先把整个过程目录扫一遍,构建一个“存储过程到存储过程”的调用关系,把嵌套拆出来。这样你在分析某一层的时候,如果遇到 CALL another_proc(...),你就能知道这条血缘是否可以再往下一层追踪,不至于把调用点当成解析的死角。
3. 过程体里SQL的四种“形态”,决定了你的解析策略
围绕“怎么把过程体里的SQL挖出来”这件事,我总结了一个核心经验:不要试图用一个万能解析器解决所有问题,把SQL按照“出现形态”分类,然后分别制定策略,是最稳的做法。
3.1 静态直写型SQL:用正则和边界匹配去切
最常见的一类,就是过程体里老老实实写好的静态SQL。它的特点是:执行语句和表名都是写死的,没有动态拼装,SQL结构比较规整,关键词和语法都是当前数据库的标准方言。
对这类SQL,解析策略是:先用关键词把 SQL语句从过程体里“切”出来。比如往下找 INSERT INTO、UPDATE、DELETE FROM、MERGE INTO、SELECT INTO 这些执行语句起始点,然后再找到它们对应的结束位置。“结束位置”的判断比较讲究——不能简单看行尾,因为SQL可能会换行;也不能靠数分号,因为子查询里也会有分号。比较可靠的方式,是用一个简单的词法扫描器,记录当前在不在字符串内、在不在子查询括号内,一层一层深度跟踪,遇到真正的语句结束符时再做切分。
切出SQL语句文本之后,就可以用现成的SQL解析库去解析了。字段血缘的核心逻辑都在这里:拿到一条 INSERT INTO target(a,b,c) SELECT x,y,z FROM source,你自然能推出 source.x → target.a、source.y → target.b、source.z → target.c 这样的映射关系。
3.2 临时表接力型SQL:把“过程状态”纳入血缘链路
这类SQL是存储过程血缘分析最麻烦、也最有价值的部分。一个过程里经常会有这种写法:
如果你只做单条SQL解析,你会得到两条独立结果:第一条告诉你 payment → tmp_stage_1,第二条告诉你 tmp_stage_1 和 users 流向 final_table。但这两条结果拼不到一块,因为从人类理解的角度,真实血缘链路是 payment + users → final_table。
要拼起来,就得在解析器里维护一个“临时表上下文”:
- 遇到
CREATE TABLE ... AS SELECT,就把这条SQL产生的目标表登记成一个“中间实体”,记录它的字段清单和上游来源 - 遇到
INSERT INTO tmp_stage_1,就把tmp_stage_1的字段血缘合并进上下文 - 遇到
SELECT ... FROM tmp_stage_1,就根据上下文把它替换成它的上游来源
这个过程本质上是在做“符号表传播”,在编译原理里很常见,但在SQL血缘解析里很多人没认真做。我自己的经验是:先按时间顺序把过程体里所有 CREATE、INSERT、UPDATE、MERGE 语句理出一个“语句序列”,然后一条一条处理,每处理一条就从上下文里取“当前临时表的最新字段映射”,这样最终生成的是一条不中断的完整链路。
3.3 动态构造型SQL:能拼尽拼,拼不出来就做运行时捕获
动态SQL是血缘解析的“地狱模式”。它的表现形态五花八门,有的只是拼接一个过滤条件,表名是固定的;有的却是整个SELECT语句都是拼的,表名、字段名都在变量里。
我给出一个分梯度处理的建议:
- 第一梯度:如果动态SQL只是条件拼接,核心语句还是静态文本(比如
SELECT col1 FROM table_a WHERE 1=1后面动态加上AND status = ...),那你就把主语句抽出来正常解析,动态条件忽略即可 - 第二梯度:如果拼接的是整个SQL,但拼接的片段都是字面量字符串,没有变量插入,你可以写一个“变量替换器”,先提取变量赋值语句,再把变量代入动态SQL模板,还原出最终的SQL文本。这种在简单场景下能work,前提是变量赋值语句能静态推断出来
- 第三梯度:如果变量来自外部参数、结果集,或者经过了复杂计算,静态完全推不出来。这时候我的建议是不要硬凹,改成在数据库端做“执行日志捕获”。比如MySQL开
general_log或者用事件调度器去抓执行过的SQL,Hive则可以通过hooks抓取执行计划,这样能拿到真实执行时的SQL,弥补静态解析的盲区
3.4 游标循环型SQL:按顺序拆解,别被循环迷惑
存储过程里另一个很常见的模式,是用游标逐行处理数据,循环体里再执行更新或插入:
这种代码在执行的时候,游标查询和循环体内的更新是分开执行的。血缘上面,orders 表提供了驱动数据,order_log 表被更新,一条清晰的链路就是 orders → order_log。你不需要去模拟循环执行了多少次,只需要把同一个过程体内的 SELECT 和 UPDATE 当作同一个“执行单元”里的读写操作,把它们连起来就行。
所以我处理游标循环时,策略很简单:把游标声明的SELECT语句、游标循环体里的DML语句,全部当成独立SQL语句正常解析,最后按照过程体执行顺序做血缘合并。注意识别 OPEN、FETCH、CLOSE 这类关键词,避免把游标操作当成正常SQL。
4. 字段级血缘的关键:怎么把“表级血缘”升级成“字段级血缘”
表级血缘告诉你 A表 → B表,字段级血缘告诉你 A.a → B.b、A.b + A.c → B.d。显然,后者才是有价值的。但字段级血缘的解析难度比表级高很多,尤其是遇到计算、别名、多表JOIN的场景。
4.1 解析INSERT语句的列映射关系
存储过程里最常见的落数语句是 INSERT INTO target(col1, col2) SELECT expr1, expr2 FROM source。要提取字段血缘,你得先解析出目标表的字段列表,然后按顺序和SELECT列表对齐。但这里有个关键坑:很多存储过程写 INSERT INTO target 时候不写目标字段列表,直接写 INSERT INTO target SELECT ...。这种情况下,目标字段顺序完全依赖于表结构定义。
我的建议是:如果解析器能力允许,去拉取目标表的系统表结构,按列定义顺序和SELECT列对齐;如果拉不到,就先标记为“表级血缘”,不强行编造字段对应关系。强扭的字段血缘反而会误导后续的消费者,不如给一个“待确认”的状态更安全。
4.2 处理SELECT列表中的函数和表达式
血缘并不总是 source.col 直接搬到 target.col。过程体里大量存在这样的写法:
这里 target.actual_amount 的上游不是单一字段,而是 source.amount 和常量0参与的计算。规范的血缘解析器会生成一个表达式树,节点里挂上 COALESCE、*、0.9 这些操作符,而不是简单粗暴地写 source.amount → target.actual_amount。
这个度怎么把握呢?我个人的经验是:如果你做的是“数据地图”这种偏展示的场景,知道来源字段就够了;如果你做的是“数据质量分析”或者“影响分析”,那就需要把表达式完整保留下来。所以解析器至少要能做到:识别SELECT列表里哪些是直接字段引用、哪些是函数表达式,并对每项输出一个“血缘向量”,比如 [source_table.amount, CONST_0] 这样的结构。
4.3 多表JOIN场景下的字段归属追踪
JOIN是另一个难点。一条SQL里JOIN了四五张表,你要准确判断每个目标字段到底来自哪张源表,就不能只看SELECT字段名,还要分析JOIN条件和表的别名。
我的做法是,在解析SQL之后,先建立一个“别名到表”的映射表。比如 FROM orders o JOIN users u ON o.user_id = u.id,解析器就知道 o 指代 orders,u 指代 users。然后SELECT列表里的 o.amount 就能准确解析成 orders.amount。如果遇到不带表前缀的裸字段(比如直接写 amount),就要查一下当前FROM子句里哪张表有这个字段。如果多个表都有,就标记为“冲突字段”,需要人工确认,不能猜。
这一步做扎实了,整个血缘解析的准确率才有保障。
4.4 跨存储过程的血缘串联
单条SQL解析完成只是第一步,更高级的场景是跨过程串联。举个例子,p_ods 过程把原始数据写入 ods_order,p_dwd 过程读 ods_order 加工成 dwd_order,p_ads 过程再读 dwd_order 汇总成报表。你要生成一个端到端的血缘,就得把三个过程的血缘结果“拼接”起来——拼接的锚点就是共同使用的中间表。
实际操作里,我会导出所有过程的解析结果,统一存储成“表 → 字段映射”的结构,然后按照中间表名称做一次全局合并。这个环节考验的不是解析能力,而是“数据模型设计”能力。血缘本质上是一张有向图,你要把表设计成“节点表”和“边表”,然后通过图遍历算法去拼接全链路。很多工具在这一步做得很烂,原因就在于底层存储模型没设计好。
5. 四类数据库的存储过程语法差异,解析器必须得“会方言”
我前面提到的都是通用思路,但实际写代码的时候,你会发现MySQL、Oracle、SQL Server、Hive语法差别很大。如果你只想支持一个库,那好办;如果想做一个能横跨多库的解析器,就必须把这些差异单独处理。
| 对比维度 | MySQL | Oracle | SQL Server | Hive |
|---|---|---|---|---|
| 存储过程定义 | CREATE PROCEDURE ... BEGIN ... END |
CREATE OR REPLACE PROCEDURE ... AS ... BEGIN ... END |
CREATE PROCEDURE ... AS BEGIN ... END |
Hive 2.x+ 支持 CREATE PROCEDURE,但不常用 |
| 动态SQL语法 | PREPARE stmt FROM @sql; EXECUTE stmt; |
EXECUTE IMMEDIATE v_sql |
EXEC sp_executesql @sql |
hiveconf + 拼接SQL |
| 变量赋值 | SET @var = 1 |
v_var := 1 |
SET @var = 1 |
SET hivevar:name = value |
| 临时表 | CREATE TEMPORARY TABLE |
GLOBAL TEMPORARY TABLE |
#temp / ##temp |
内部表加tmp_前缀 |
| 游标循环 | DECLARE cur CURSOR FOR ... |
CURSOR FOR ... LOOP ... END LOOP |
DECLARE cur CURSOR FOR ... + WHILE @@FETCH_STATUS = 0 |
不太常用 |
从表格能看出,不同数据库的语法差异主要集中在“控制结构”和“动态SQL”这两块。你在写正则切分和词法分析的时候,一定要把数据库方言参数也传进去。比如Oracle里赋值号是 :=,MySQL里是 =,如果你只按其中一种写匹配逻辑,解析另一个库的过程体时就会全盘崩掉。
还有一个细节:Oracle的存储过程里可以嵌套子过程或包(Package),过程体里还会内嵌 BEGIN ... EXCEPTION ... END 异常处理块。解析时要把 EXCEPTION 部分也纳入SQL提取范围,不然会漏掉一部分逻辑。
6. 实操演示:从零挖出一个MySQL存储过程的血缘
前面讲了一堆方法论,这块我拿一个实际的MySQL存储过程来完整走一遍,让大家能照着复现。
6.1 准备一个带各种“坑”的示例存储过程
我造了一个日常报表加工的存储过程,里面涵盖了静态SQL、临时表、变量拼接、动态SQL几种典型写法和坑点,基本能代表一大类真实场景。
这个过程体不算复杂,但已经足够展示血缘解析的整套流程了。
6.2 分步骤抽取SQL并建立临时表上下文
基于我前面的清洗和切分思路,第一步先把过程体里的注释去掉,字符串字面量统一标记。然后按执行顺序扫描出所有确认的数据操作语句:
CREATE TEMPORARY TABLE tmp_active_users AS SELECT ...CREATE TEMPORARY TABLE tmp_user_payment AS SELECT ...INSERT INTO rpt_user_overview ... SELECT ...UPDATE rpt_user_overview ...(动态SQL还原后)
处理第一条时,解析器得到一张新的临时表 tmp_active_users,其字段列表为 (user_id, name, reg_date),上游来源分别为 users.id、users.name、users.reg_date。同时它记录下这张临时表的字段映射关系,存进临时表上下文。
处理第二条时,解析得到 tmp_user_payment,字段列表为 (user_id, pay_cnt, total_amount),上游来源分别是 payment.user_id、COUNT(*)、SUM(payment.amount)。注意这里的 pay_cnt 不是一个直接字段引用,而是一个聚合函数,血缘解析要记录成“来自 payment 的 count计算”,不能只写 payment.pay_cnt(这张表里压根没有这个字段)。
处理第三条时,INSERT的目标表是 rpt_user_overview,但它源表用了临时表。这时候就要回查临时表上下文:
rpt_user_overview.user_id ← tmp_active_users.user_id ← users.idrpt_user_overview.pay_cnt ← tmp_user_payment.pay_cnt ← COUNT(*) over paymentrpt_user_overview.total_amount ← tmp_user_payment.total_amount ← SUM(payment.amount)
到这里,一个跨临时表的完整血缘链路就打通了:users + payment → rpt_user_overview。
处理第四条时,动态SQL的模板里有 UPDATE rpt_user_overview,所以它本质上是对 rpt_user_overview 的一次原地更新,新增了一个 pay_flag 字段,来源是 total_amount 的判断表达式。解析器应该把它识别成 rpt_user_overview 的字段自更新,并挂上表达式来源,而不是把它误判成生成新表。
6.3 把解析结果输出成标准血缘模型
解析完毕之后,建议把结果落成下面这种标准结构,方便后续做全链路拼接。
表级血缘结果:
| 源表 | 目标表 | 处理方式 | 所在过程 |
|---|---|---|---|
| users | tmp_active_users | CREATE AS SELECT | sp_daily_user_report |
| payment | tmp_user_payment | CREATE AS SELECT | sp_daily_user_report |
| tmp_active_users | rpt_user_overview | INSERT SELECT | sp_daily_user_report |
| tmp_user_payment | rpt_user_overview | INSERT SELECT | sp_daily_user_report |
| rpt_user_overview | rpt_user_overview | UPDATE(动态SQL) | sp_daily_user_report |
字段级血缘重点链路:
| 目标字段 | 来源字段 | 转换逻辑 |
|---|---|---|
| rpt_user_overview.user_id | users.id | 直接映射 |
| rpt_user_overview.user_name | users.name | 直接映射 |
| rpt_user_overview.pay_cnt | payment(行数) | COUNT(*) |
| rpt_user_overview.total_amount | payment.amount | SUM聚合 |
| rpt_user_overview.pay_flag | rpt_user_overview.total_amount | CASE WHEN >= 1000 |
这个结构出来以后,你就可以把它导入Neo4j等图数据库,或者存到关系表里做全链路影响分析。业务方拿到的,就是一串能讲清楚故事的血缘路径。
7. 常见问题与排查技巧实录
挖存储过程血缘这件事,坑是真的多。这里列几个我实际踩过的坑,希望能帮大家少走弯路。
7.1 为什么解析出来的血缘总是少了一段
如果你的解析结果里,经常出现某张目标表的上游是空、或者断在某一层,最常见的两个原因:
一是没有处理临时表上下文。比如过程体里先 CREATE TEMPORARY TABLE tmp_a,后面 INSERT INTO final_tbl SELECT ... FROM tmp_a。如果解析器没有把 tmp_a 的字段来源记录在案,那 final_tbl 的上游就断了。解决办法就是维护好“临时表上下文”,每处理完一条建表语句,就更新上下文。
二是动态SQL拼接太复杂,静态解析不出真实来源。这种我建议引入运行时捕获,通过打开数据库的执行日志或审计插件,收集实际执行过的SQL。等收集到一批真实SQL后,再回填到血缘系统里。动态SQL对血缘来说,永远不能只靠猜。
7.2 解析结果不准,怎么定位问题
先缩小范围,看问题出在“切分”还是“解析”。我自己的排查习惯是:
- 在清洗和切分阶段,输出中间结果:把识别出来的执行语句列表打印出来,人工比对一遍,看有没有多切、漏切
- 在字段血缘层面,选一个典型存储过程,手动整理它的血缘链路,然后和解析结果做diff,定位是哪个SQL环节出了问题
- 如果切分准确但字段血缘不对,大概率是SQL解析库对某些方言语法支持不完善,比如Oracle的
(+)外连接、MySQL的ON DUPLICATE KEY UPDATE,这些很容易让通用解析器出问题
排查问题一定要带着“中间结果可见”的思路去做,别拿最终结果直接对答案,那样盲区太大。
7.3 解析性能太差,几百个存储过程跑不动
存储过程源码动辄几百上千行,加上要解析多条SQL,如果实现不当,性能确实会很难看。有几个优化思路:
- 先做文本级的粗过滤,比如只识别包含
INSERT、UPDATE、DELETE、CREATE关键词的存储过程,其他根本不用进详查阶段 - 使用并行解析。不同存储过程之间理论上互不干扰(除开跨过程血缘拼接环节),可以按过程为单位做多线程并行
- 字段级解析的SQL解析库非常耗内存,建议逐条SQL解析、立即落库、释放对象,别一次性把整个过程体的所有SQL都解析完再存
7.4 数据库版本升级后,原来能解析的突然不对了
这个坑特别隐蔽。比如SQL Server从2014升到2019,系统表结构或者某些系统视图字段可能有变动,你原来写的查询存储过程源码的SQL可能会受到影响。Oracle也一样,ALL_SOURCE 的结构相对稳定,但有些版本的 DBMS_METADATA.GET_DDL 返回值格式会变。
我的建议是:所有“取源码”的SQL,都封装成一个独立模块,并写自动化测试用例。每次数据库升级后,跑一遍测试用例,确认取回来的源码格式和以前一致,再做全量血缘解析。否则已经入库的旧血缘关系和新的解析结果会打架,影响数据地图的稳定性。
8. 血缘解析之后,你还能把这件事往深了做
写完核心解析器只是开始,这块能力还有几个非常有价值的延展方向。
第一个方向是“影响分析”。血缘图本质是一张有向图,你能顺着“下游消费”方向查:如果我要改 users 表的 id 字段类型,有哪些存储过程、哪些报表会受影响?这对数据平台做变更管理特别有用。做法也不难,把字段级血缘图存储成图形结构,遍历所有从 users.id 出发的边,就能得到完整的下游影响清单。
第二个方向是“指标口径回溯”。很多公司在做数据治理时,最头疼的问题就是“指标口径对不上”。同样是“活跃用户数”,A报表和B报表可能定义完全不同。如果你把存储过程的血缘解析做到字段级,就能追溯每个指标的计算来源,看到它是 COUNT(DISTINCT user_id) 还是 COUNT(*),还是带了一堆复杂的过滤条件。这对对齐口径是真正的杀手锏。
第三个方向是“存储过程质量分析”。血缘图里能暴露很多设计问题,比如某张表被几十个存储过程直接读取、形成“扇入过大”的耦合点;或者某个存储过程一层套一层,依赖链特别深,改一个字段要冒巨大风险。这些通过血缘结构都能量化出来。做数据平台的人,完全可以基于这些指标去推动存储过程重构。
我这两年做数据治理项目,最直观的感受是:数据血缘不该是静态的“展示品”,它应该是一个能回答“为什么”的操作系统。而存储过程这道坎,恰恰是把系统做踏实的关键。这篇分享的这些方法和坑,都是我在实际项目里一步一步磨出来的,希望能给正在做或者准备做这块的朋友省下一些排查时间。如果你遇到比动态SQL还要诡异的写法,或者有什么更好的解析思路,欢迎交流讨论。