把血缘嵌进 CI/CD:上线前自动评估 SQL 变更影响
Esther 数据治理实战 · 第 16 篇。代码合入有 CI 把关:编译不过不合、单测红了不合。SQL 变更呢?多数团队还是评审人凭经验扫一眼。这篇讲一件具体的事:怎么把列级血缘分析接进发布流水线,让每条要上线的 SQL 先过一遍机器检查。
一、SQL 变更上线前,靠什么把关
数据变更和代码变更有个不对称:代码改错了,编译器和单测在合入前就拦住,轮不到线上告警;SQL 变更基本没有这道防线。
几种常见剧情:
- ETL 里 JOIN 条件写错,汇总数字悄悄错了一个月,等业务对账才发现;
- DELETE 少写了半个 WHERE 条件,影响范围比预期大出一截;
- 字段类型一改,下游取数脚本接连报错,改一处牵出一片。
共同点:出事的地方不在提交的那条 SQL 里,在它的下游。下游在哪,靠"我记得"。评审人扫一眼能看出语法和逻辑问题,看不出全企业的影响面——这不是认真不认真的问题,是缺一份机器能查的清单。
列级血缘就是这份清单:一条 SQL 读了哪些表的哪些列、写了哪些表的哪些列、经过什么转换,全部结构化。有了它,"上线前自动评估 SQL 变更影响"才谈得上落地。
二、机器能替人看什么
把一条 SQL 交给分析接口,返回的结构化结果能回答三个问题:
| 问题 | 从返回里看什么 |
|---|---|
| 这条 SQL 解析得干净吗 | warnings:解析器标出的问题点,定位到具体行列 |
| 它读了谁、写了谁 | nodes+edges:列到列的血缘,含转换表达式 |
| 血缘解析出了多少东西 | 节点/边的数量——空结果本身是个信号 |
第三条值得单独说。一个几百行的脚本解析出 0 个节点、0 条边,通常意味着脚本里有大段平台处理不了的语法——这种"带伤上线"的 SQL,正是要在合入前拦下来的。
三、集成点:一个分析接口
集成的全部动作是向分析接口 POST 一条 SQL。示例用一条教科书式的入库 SQL:
curl-s-XPOST http://esther.internal:8000/api/analyze\-H'Content-Type: application/json'\-d'{ "sql": "INSERT INTO dws.order_summary (order_id, user_name, total_amount) SELECT o.id, u.name, o.price * o.quantity FROM ods.orders o JOIN ods.users u ON o.user_id = u.id", "dialect": "mysql" }'返回(真实结果,截选;完整结构还含 relations、metadata_tree 等字段,本例共 23 个节点、14 条边):
{"analysis_id":"1805306ebb","dialect":"mysql","warnings":[],"nodes":[{"id":"ods.orders.id","type":"COLUMN","name":"id"},{"id":"ods.users.name","type":"COLUMN","name":"name"},{"id":"dws.order_summary.order_id","type":"COLUMN","name":"order_id"},{"id":"dws.order_summary.total_amount","type":"COLUMN","name":"total_amount"}],"edges":[{"source":"ods.orders.user_id","target":"ods.users.id","type":"JOIN"},{"source":"ods.orders.price","target":"__join_0__.price","type":"DIRECT"},{"source":"__join_0__.price","target":"__insert_src__._col3","type":"TRANSFORM","transform_expr":"o.price * o.quantity"},{"source":"__insert_src__._col3","target":"dws.order_summary.total_amount","type":"DIRECT"}]}edges 里__join_0__、__insert_src__这类名字是平台标出的中间层(JOIN、INSERT…SELECT 的临时结果),端到端读结论:dws.order_summary.total_amount来自ods.orders.price * ods.orders.quantity,表达式原文就存在这条边的transform_expr字段里;order_id、user_name是直取字段,边型 DIRECT;JOIN 关联两端另有一条 JOIN 边标记。
那"不干净的 SQL"返回什么?把关键字拼错再看一次:
curl-s-XPOST http://esther.internal:8000/api/analyze\-H'Content-Type: application/json'\-d'{"sql": "SELEC * FROM ods.orders", "dialect": "mysql"}'HTTP 状态码仍是 200,但warnings里有一条实打实的PARSE_ERROR(截选):
{"warnings":[{"code":"PARSE_ERROR","message":"Invalid expression / Unexpected token. Line 1, Col: 12. ...","sql_fragment":"SELEC * FROM ods.orders","position":[1,12,1,24],"severity":"WARNING"}]}注意:拼写错误不会让接口吐一个含糊的 5xx——平台把"解析不干净"作为结构化警告返回,定位到行列,同时血缘为空。对 CI 来说这反而好用:判定逻辑读warnings和节点数就够了,不用猜错误码。
四、把检查规则写进流水线
判定规则三句话:
- 请求失败(非 200)→ 拦。服务不可用或请求本身有问题,不能默默放过;
warnings里有PARSE_ERROR→ 拦。这条 SQL 平台解析不干净,带伤上线后查血缘会是空白;- 节点/边为空 → 告警。多半有大段平台吃不了的语法,人工看一眼再定。
落成流水线里的一步(示意脚本,GitLab CI / Jenkins 通用思路):
# 示意:对本次提交涉及的 .sql 文件逐个检查forfin$(gitdiff--name-only HEAD~1 --'*.sql');doresp=$(curl-s-w'\n%{http_code}'-XPOST"$ESTHER_URL/api/analyze"\-H'Content-Type: application/json'\-d"$(jq-Rs--argd"$DIALECT"'{sql: ., dialect: $d}'"$f")")code=$(echo"$resp"|tail-1);body=$(echo"$resp"|head-n-1)if["$code"!="200"];thenecho"✗$f:请求失败 (HTTP$code)";exit1;fiifecho"$body"|jq-e'.warnings[]? | select(.code == "PARSE_ERROR")'>/dev/null;thenecho"✗$f:存在解析错误";exit1fin=$(echo"$body"|jq'.nodes | length')echo"✓$f:解析出$n个节点"done这段是接入方的胶水代码,不是产品的一部分。判定阈值按团队自己的发布纪律调:警告是只告警还是拦截、要不要只盯写操作,都可以配。建议从严到宽分两步——先只告警跑两周看误报率,再升级为拦截。
五、再进一步:跟库里已有的血缘接起来
门禁脚本看到的是"这一条 SQL"的血缘。下一层的问题是:我要改ods.orders,已经在生产上跑着的那些下游谁受影响?这要在平台已沉淀的血缘图上查:
GET /api/v1/lineage/ods.orders?asset_type=table&direction=downstream&depth=3Authorization: Bearer<token>返回从这张表往下游展开指定层数的节点和边。前提有两个:先用平台账号换取 token;更关键的是平台里得先有这份数据源的血缘——它来自平台对数据源的持续采集与血缘分析。门禁查的是单条 SQL,目录查的是全量资产,两层拼起来,"改之前先看看谁会炸"才有完整答案。
六、边界交底
两个边界说在前面:
- 分析接口是无状态的。发一条分析一条,不落库、不与历史对比。要在全量资产上做上下游查询,走第五节那条目录查询的路,依赖平台里的存量血缘;
- 血缘看结构,不看数据。JOIN 关联错表、字段类型改了、下游被误删,这类结构性问题血缘拦得住;聚合逻辑算错数、WHERE 值条件写不对,血缘无能为力——那要靠数据核对与质量规则,是另一层防线。
七、写在最后
SQL 变更的把关,缺的不是流程,是那份机器能查的影响清单。列级血缘接口把清单给了出来,剩下的是把它接进流水线的几十行胶水代码——评审人从"猜影响"变成"看报告",拦截发生在合入之前,而不是发生在下个月的对账会上。
▎Esther是一个企业级 SQL 数据血缘分析平台:28 种 SQL 方言、27+ 种数据源元数据采集、列级血缘、元数据管理、私有化部署。