46.5 数据治理与血缘
一句话
血缘回答“改这张表/这一列,会炸到谁”:表级血缘用于评估影响面,列级血缘用于定位口径来源——没有血缘的治理,就是靠人问“这个字段谁在用”。
治理的四件事
| 事项 | 解决什么 | 最小可用做法 |
|---|---|---|
| 血缘 | 改动影响面、故障定位 | 解析 ETL/查询日志,建上下游图 |
| 指标字典 | 口径不一致 | 每个指标写“定义 + 公式 + 负责人 + 来源表” |
| 质量校验 | 数据错了没人知道 | 行数/空值/枚举/波动阈值告警 |
| 权限与审计 | 谁能看、谁看过 | 按分级授权 + 访问日志(38.5) |
代码:血缘图的爆炸半径
package main
import (
"fmt"
"sort"
)
// 表级血缘:from -> to
var edges = map[string][]string{
"ods_orders": {"dwd_orders", "dwd_order_item"},
"ods_users": {"dwd_users"},
"dwd_orders": {"dws_sales", "ads_report"},
"dwd_order_item": {"dws_sales"},
"dwd_users": {"dim_user", "dws_user_value"},
"dws_sales": {"ads_report", "ml_feature_sales"},
"dws_user_value": {"ads_report"},
"dim_user": {"ads_report", "ml_feature_user"},
"ads_report": {"dashboard_gmv", "dashboard_ops"},
"ml_feature_sales": {"model_forecast"},
"ml_feature_user": {"model_churn"},
}
// 哪些节点是“最终消费方”
var consumers = map[string]bool{
"dashboard_gmv": true, "dashboard_ops": true, "model_forecast": true, "model_churn": true,
}
func downstream(start string) (all []string, byLevel map[int][]string) {
byLevel = map[int][]string{}
seen := map[string]bool{start: true}
level := map[string]int{start: 0}
queue := []string{start}
for len(queue) > 0 {
cur := queue[0]
queue = queue[1:]
for _, nxt := range edges[cur] {
if seen[nxt] {
continue
}
seen[nxt] = true
level[nxt] = level[cur] + 1
byLevel[level[nxt]] = append(byLevel[level[nxt]], nxt)
all = append(all, nxt)
queue = append(queue, nxt)
}
}
return
}
func main() {
for _, src := range []string{"ods_orders", "dwd_users", "dws_sales"} {
all, byLevel := downstream(src)
cons := 0
for _, n := range all {
if consumers[n] {
cons++
}
}
fmt.Printf("=== 改 %s 会影响 %d 个下游节点、%d 个最终消费方 ===\n", src, len(all), cons)
var levels []int
for l := range byLevel {
levels = append(levels, l)
}
sort.Ints(levels)
for _, l := range levels {
nodes := byLevel[l]
sort.Strings(nodes)
fmt.Printf(" %d 跳:%v\n", l, nodes)
}
fmt.Println()
}
}
本机实测(macOS,Apple M1,Go 1.27):
=== 改 ods_orders 会影响 8 个下游节点、3 个最终消费方 ===
1 跳:[dwd_order_item dwd_orders]
2 跳:[ads_report dws_sales]
3 跳:[dashboard_gmv dashboard_ops ml_feature_sales]
4 跳:[model_forecast]
=== 改 dwd_users 会影响 7 个下游节点、3 个最终消费方 ===
1 跳:[dim_user dws_user_value]
2 跳:[ads_report ml_feature_user]
3 跳:[dashboard_gmv dashboard_ops model_churn]
=== 改 dws_sales 会影响 5 个下游节点、3 个最终消费方 ===
1 跳:[ads_report ml_feature_sales]
2 跳:[dashboard_gmv dashboard_ops model_forecast]
读数:
| 源表 | 下游节点 | 最终消费方 | 结论 |
|---|---|---|---|
ods_orders | 8 | 3 | 改它等于改全站报表 + 两个模型 |
dwd_users | 7 | 3 | 影响用户维度、用户价值模型与流失模型 |
dws_sales | 5 | 3 | 影响 GMV 看板与预测模型 |
🔑 血缘的价值不是“画图”,而是变更前的检查清单:改
ods_orders的字段 → 自动列出 8 个下游 + 3 个消费方 → 通知负责人 → 排期。没有这张图,只能靠“谁记得”。
列级血缘:口径问题的唯一解
| 问题 | 表级血缘能否回答 | 列级血缘 |
|---|---|---|
| 改表会影响哪些任务 | ✅ | — |
| 这个指标的“销售额”来自哪张表的哪列 | ❌ | ✅ |
| 两个报表的同名指标为什么不一样 | ❌ | ✅(追到不同来源列/不同过滤条件) |
⚠️ 列级血缘的实现成本高(要解析 SQL AST),但这是“口径争议”唯一能给出证据的东西。折中做法:先覆盖核心指标(GMV、DAU、转化率)的列级血缘,其余保持表级。
反模式
| 反模式 | 后果 |
|---|---|
| 血缘靠人工维护文档 | 三个月后与真实链路脱节 |
| 只有表级血缘,没有列级 | 指标口径争议无法定位 |
| 改表前不查血缘 | 上线后下游报表全空 |
| 指标没有负责人 | 口径错了没人认领 |
| 质量校验只看行数 | 行数正常但金额算错,照样上线 |
| 血缘图不包含“外部依赖”(API/文件) | 影响面漏一半 |
自检
Q1 为什么表级血缘不够,还需要列级血缘?
▸ 答案:表级只回答“这张表的下游是谁”,回答不了“这个指标用了哪一列、经过什么过滤”。口径问题(同名指标不同值)往往源于列级差异:一个用 amount,另一个用 amount - refund;一个含税一个不含。只有列级血缘能追到源头,给出“为什么不同”的证据。
Q2 血缘图怎么自动生成,而不是靠人填?
▸ 答案:从执行记录里解析:① 数据仓库的查询日志/执行计划(解析 SQL 的源表与目标表);② ETL 调度系统的任务定义(输入/输出表);③ 存储过程的 AST 解析。列级血缘需要 SQL 解析器(如 sqlparse/Calcite),成本更高。关键是“自动 + 定期刷新”,人工维护必然过期。
Q3 一个有 200 张表、500 个任务的数仓,血缘最该先覆盖什么?
▸ 答案:先覆盖核心指标链路(GMV、订单量、DAU 等 10–20 个)和被引用最多的表(入度最高的 20 张)。这两类覆盖了 80% 的变更风险与口径争议;长尾表可以只做表级。判断依据是“出问题时会不会有人问‘这数从哪来的’”。