2954 字
约 9 分钟
23
S13-A12 · 数据质量的责任链:脏数据从哪来、谁修

"这个月活跃用户怎么少了四千?"报表会上,增长说按他的口径没少,数据说按数仓口径就是少,产品说两个数都不是他要的。讨论三天,最后发现三份报表的分母不同——没有一个人的数字是错的,但没有一个能用来决策。

麻烦不在修,在认账:链路三四段,每段都有合理理由,每段的负责人都不认为后果落在自己身上。这篇拆责任链——脏数据从哪来、判据是什么、谁该动手。


1. 四种来源与归因难点

来源 典型表现 归因难点
采集端 客户端时钟错、必填字段缺失、重复上报 日志只记成功,异常被 SDK 吞掉
转换端 关联键丢失、类型截断、时区错位 中间层无断言,错到下游才显形
消费端误用 拿曝光数当去重用户数 使用方不懂字段语义,也没有拦截
定义分歧 三个部门三个"活跃用户" 无权威定义,谁报表响谁赢

前两类是工程问题,后两类是治理问题,但症状一样:数字不对。归因第一步不是查数据,而是判断"这属于哪一类"——把定义分歧当采集 bug 修,会白改一周埋点。


2. "谁产生谁负责"为何经常失效

这条原则听着无懈可击,落地时失效在四处:

环节 产生方看到 动力在哪
埋点 事件发出去了,接口 200 上报成功即完成,不看下游
转换 任务跑完了,无报错 无断言,脏数据静默通过
消费 报表能出数 出数不等于对,无人对账
决策 数字与预期不符 先质疑数据源,再质疑自己

核心矛盾是产生方离后果太远:埋点工程师看不到"缺了 device_id,归因报告算丢了 12% 转化"。责任要可执行,必须把后果搬到产生方眼前——契约、对账、下游回传的失败样例,而不是一句"你负责"。


3. 数据契约:把语义写成可校验的东西

要素 必须写清 不写的后果
字段语义 这个字段到底度量什么 同名不同义,跨表 join 出错
取值范围 枚举、上下界、单位 分出"元/分"级错误
空值语义 NULL 是缺失、未知,还是零 均值被 NULL 拉偏或吞掉
更新频率 实时/小时/日,延迟上界 下游按 T+1 用实时表,读到半截
# 契约即代码:可被 CI 校验,也可生成文档
table: fact_user_action
owner: growth-data
freshness: {mode: incremental, max_delay: 2h}
columns:
  user_id:    {type: string, nullable: false, semantics: 设备首次登录后绑定的用户标识}
  event_time: {type: timestamp, tz: UTC, nullable: false, skew_max: 5m}
  channel:    {type: enum, values: [organic, paid, referral, unknown], default: unknown}
  amount:     {type: decimal, unit: cent, min: 0}

契约的价值不在文档,在它能在流水线里被执行:nullable、enum、unit、max_delay 都能变成一条断言,断言失败就阻塞发布。


4. 校验放在哪里

位置 严格度 代价
写入端(采集/入库) 高,脏数据进不来 灵活度低,新字段要改 schema
转换端(ETL/建模) 中,可分级拦截 需要维护断言,误报要有人处理
消费端(报表/模型) 高,但重复 每个消费方各写一遍,口径漂移

判据只有一条:错误代价是否随链路放大。放大得厉害(金额、用户标识、时间戳)就在写入端拦;不放大(可选标签)就在消费端过滤。

全放写入端,业务会被 schema 变更拖死;全放消费端,等于同一件事做 N 遍且每次不同。常见组合是关键字段写入端强校验 + 其余转换端分级校验 + 消费端兜底过滤。


5. 不是所有脏数据都要修

级别 判据 处理
致命 会导致错误决策或错误扣款 阻塞发布,必须修
可容忍 影响局部展示,不改变结论 记录、监控、定期清理

判据是**"会不会导致错误决策"**,不是"是否干净"。3% 空值率若只影响一张下钻明细表,修它收益接近零;同样 3% 落在 GMV 分母上,就是 P0。

治理最常见的失控方式是追求全域零脏数据:成本线性上升,收益迅速归零,最后团队学会绕过校验。


6. 发现机制与它们的成本

机制 发现什么 成本
对账 上下游总量、金额、条数是否一致 需要独立数据源,维护对账规则
分布监控 空值率、枚举占比、分位数漂移 需要基线,误报要调阈值
抽样核对 人工看具体记录 人力,覆盖率低但能发现语义错误

三者互补:对账抓总量缺口,分布监控抓形态突变,抽样核对抓"数字对但意思错"。只有对账会漏掉语义错误,只有抽样会漏掉规模问题。

-- 分布监控的最小可用形式:按天看关键字段的空值与形态
SELECT dt,
       COUNT(*)                                        AS rows,
       SUM(CASE WHEN user_id IS NULL THEN 1 ELSE 0 END) / COUNT(*) AS null_rate,
       COUNT(DISTINCT channel)                          AS channel_cardinality,
       APPROX_PERCENTILE(amount, 0.5)                   AS p50_amount
FROM   fact_user_action
WHERE  dt >= CURRENT_DATE - 7
GROUP  BY dt
ORDER  BY dt;

channel_cardinality 突然从 4 变成 40,通常不是业务变化,是上游枚举被写坏了——这类信号比空值率更早出现。


7. 修复的责任划分

动作 谁做 不该谁做
改代码/改埋点 产生方(业务研发) 数据团队代改,改完没人认
补历史数据 数据平台 + 产生方确认口径 单独由平台猜口径回刷
通知下游 发现方,且必须落到具体人 发到公共群,无人认领
判定是否需要重算 指标 owner 由报数的人自行决定

补历史数据最易出事:回刷口径若与当初不一致,历史曲线会出现无法解释的台阶,之后每次同比都要解释。所以回刷必须留"口径版本 + 区间 + 影响面"三样记录。

# 修复工单:三件事写不清,下游就无从判断数据还能不能用
issue:  {table: fact_user_action, field: channel, window: "2026-10-01..10-07"}
cause:  upstream enum written as lowercase      # 归因结论,不是猜测
fix:    {code: MR-1234, backfill: true, scope: affected_partitions_only}
impact: {metrics: [gmv_daily, funnel_report], notify: [growth, exec-report]}
verify: {compare: rows_upstream vs rows_downstream, tolerance: 0}

8. 血缘与影响面

没有血缘,修复只能靠经验猜:"改这个字段应该没事吧?"然后某个下游看板静默归零两周。

-- 字段级血缘:给定字段,向上找直接消费者(示意结构,按自家元数据表核对)
WITH RECURSIVE up AS (
  SELECT table_name, column_name, 0 AS depth
  FROM   lineage_edges WHERE source = 'fact_user_action.channel'
  UNION ALL
  SELECT e.table_name, e.column_name, up.depth + 1
  FROM   lineage_edges e JOIN up ON e.source = up.table_name || '.' || up.column_name
  WHERE  up.depth < 5
)
SELECT * FROM up ORDER BY depth;

血缘的用途有三个:评估改动影响面、定位"这个数从哪来"、故障时决定先停哪张表。不必精确到每次查询,但必须覆盖源表到核心指标的主链。


9. 定义分歧:最难的一类

同一个"活跃用户",增长按"7 日内有登录",数据按"7 日内有任意事件",产品按"7 日内有核心行为"。三个都合理,合起来就是三份报表。

处理方式不是统一口径(往往统一不了),而是把口径写成带版本的独立对象,让报表显式引用:

metric: active_user
version: v3
definition:
  numerator: count(distinct user_id)
  denominator: null            # 比率型指标才需要
  window: 7d
  dedup_key: user_id
  filters: [event_type in (login, core_action), is_bot = false]
  owner: growth-data
  supersedes: v2               # v2 为"任意事件",保留至 2026-Q2
  recompute: false             # 口径变更不回刷历史,改用双线并行

只写名字不写这六项(分子、分母、时间窗、去重键、排除条件、owner),等于把解释权留给会议室。口径变更必须留旧版本,否则历史曲线断成两截,分析全部作废。


10. 度量

指标 定义要点 健康信号
数据事故数 按"是否影响对外数字"分级 下降,且致命级趋零
平均发现时长 从数据出错到被发现 缩短,靠监控而非靠人反馈
平均修复时长 从发现到下游恢复可信 缩短,含回刷
对账覆盖率 核心表中有对账规则的比例 上升,优先覆盖对外指标

第二个指标最能说明水平:发现时长以周计,实际是在等业务方来投诉——那叫客服,不叫质量体系。


11. 排错:数字不对时的排查顺序

现象 先查什么 常见结论
总量差一截 上游条数 vs 下游条数 过滤条件被新增,或分区漏读
某天骤降 任务是否成功、分区是否存在 上游延迟,读到半截数据
同比失真 口径版本是否变更 变更未回刷,或回刷区间错
只有一张报表错 该报表的 join 与去重键 多对多放大,重复计数

最后一条很典型:join 放大产生的错误数字看起来完全合理,它只是把一部分行数了两次。去重键写进契约,就是为了让它在建模阶段暴露。


12. 边界:什么时候不必建治理体系

场景 判断 替代做法
数据不参与决策 错也无后果 只做基本校验
单团队自产自用 责任链只有一环 口头约定 + 抽样自查
数据量极小可人工核对 工具成本高于人力 每月人工对一次

判据:**当"错一次"的代价低于"建体系"的成本,先别建体系,先把错一次的代价算清楚。**治理的门槛是后果,不是数据量。


13. 自测

  1. 四类来源里,为什么把定义分歧当采集 bug 修会白改一周?
  2. "谁产生谁负责"失效的核心原因是什么,怎么让责任可执行?
  3. 数据契约的四个要素里,哪一个最容易被漏掉且后果最隐蔽?
  4. 校验放在写入端还是消费端,判据是什么?
  5. 什么样的脏数据可以不修?判据是哪一句?
  6. 发现时长长期以周计,说明当前实际靠什么在发现数据问题?
S13-A12 · 数据质量的责任链:脏数据从哪来、谁修
https://www.clxhxhhr.top/posts/6254/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。