DD 数据开发学习站DATA ENGINEERING
模块 03 · 数据仓库

数据质量与治理

质量五维度、6 类检查 SQL、异常处理,以及元数据、血缘、权限与 SLA。

难度 · 进阶 约 90 分钟 必修 练习 10 项 计划内 刷题 0/3
建议先学:指标体系与口径
本页目录 · 37 节

本章目标:能按完整性、唯一性、一致性、准确性、及时性五个维度设计检查,写出 6 类核心校验 SQL,并给出丢弃、填充、告警、隔离的处理方案。 前置知识:数据仓库入门、指标口径 预计用时:90 分钟

真实公司里,数据开发有相当一部分时间在处理数据质量。报表晚半小时可以忍,数字错了会直接导致决策错误。本章讲通用检查方法,真实数据里的异常案例和具体数量放在独立的项目线里处理。

1. 为什么数据质量必须前置

电商数据里常见的问题包括:

类型 位置 典型表现
业务键重复 order_reviews 同一 review_id 出现多行
取消/不可用订单仍有支付 orders × payments 状态与支付记录矛盾
支付总额与明细不一致 payments vs order_items 金额对不上
时间倒挂 orders 承运早于下单、签收早于承运
状态与时间矛盾 orders 状态已送达但没有签收时间
评价时间异常 order_reviews 评价早于签收
商品缺品类 products 品类为空
订单没有明细 order_items 订单存在但没有商品行

不在 DWD 层挡住,评价重复会让评分虚高、样本量虚增;时间倒挂会让履约时长出现负值;无明细订单会让“订单数”和“明细行数”永远对不上。错误还会顺着 ODS → DWD → DWS → ADS 一路传染,最后没人说得清是哪一步坏的。

注意:签收时间为空不一定是脏数据。未送达的有效订单没有签收时间,属于正常业务状态;只有状态已经是 delivered 却没有签收时间才是缺陷。能区分“业务空值”和“质量缺陷”,是数据开发的入门功。

2. 数据质量的五个维度

维度 含义 检查项 期望
完整性 该有的不能少 必填时间戳、订单是否有明细、商品是否有品类 关键字段缺失可控,缺失原因可解释
唯一性 主键和业务键不能重复 orders.order_id、order_reviews.review_id 主键重复 0 行
一致性 表之间、字段之间对得上 外键、支付与明细金额、状态与支付 无孤儿行,金额差异在阈值内
准确性 值在合理范围、时间顺序正确 负金额、时间倒挂、枚举合法 符合业务规则
及时性 数据按时到达、分区齐全 分区是否生成、任务是否超时、行数波动 在 SLA 前完成且波动可解释

2.1 完整性

完整性看“应该有的值有没有”。空值要分三类:

  • 业务空值:有效订单还没送到,delivered_date 为空,保留。
  • 质量缺陷:状态已是 delivered,签收时间却为空,需要告警。
  • 可填充空值:商品品类为空,可以补 unknown,但要在报表中单独展示,不能混进正常品类排名。

2.2 唯一性

唯一性看主键和业务键是否重复。order_reviews 的 review_id 本应唯一,一旦重复,会让评价均分被重复样本带偏。

注意“一单多明细”“一单多支付”都是正常的多行:同一个订单可以有多件商品、多次支付,这是业务事实,不是重复。只有业务主键(如 review_id)重复才是问题。

2.3 一致性

一致性包括:

  • 外键一致:明细、支付、评价的 order_id 必须能在 orders 找到。
  • 状态一致:取消或不可用的订单通常不应有支付记录,发现后作为告警而非错误(可能涉及退款、分期或先支付后取消)。
  • 金额一致:订单支付总额应等于明细金额(商品价 + 运费)汇总,差异超过阈值的要逐单核查。
  • 口径一致:DWD 行数、GMV 必须和项目维护的指标基线对得上。

2.4 准确性

准确性看值是否符合业务规则:

  • 价格、运费、支付金额不能为负。
  • 运费为 0 是合法的包邮,不能当异常。
  • 时间顺序必须合理:下单 ≤ 审核 ≤ 承运 ≤ 签收。
  • 评价时间不应早于签收时间。
  • 枚举值必须在允许列表中,状态和支付方式的取值以业务字典为准。

2.5 及时性

及时性看数据是否按时到达:

  • 当天分区是否生成。
  • 任务是否在 SLA 时间前完成。
  • 行数是否比历史均值异常偏低。
  • 上游是否延迟。

及时性差的数据即使准确,也会让报表开天窗。

3. 六类核心检查 SQL

以下 SQL 以 ODS 表为例,用来发现入仓前的问题。生产上会把这些检查固化成任务,结果写入质量报告表。

3.1 主键唯一

SELECT order_id, COUNT(*) AS cnt
FROM ods_orders
GROUP BY order_id
HAVING COUNT(*) > 1;

预期返回 0 行。评价表则按 review_id 检查:

SELECT review_id, COUNT(*) AS cnt
FROM ods_order_reviews
GROUP BY review_id
HAVING COUNT(*) > 1
ORDER BY cnt DESC;

返回重复的 ID 和重复次数。明细表没有天然单列主键,同一订单多行是正常业务,只能按整行检查完全重复。

3.2 外键完整

SELECT '明细→订单' AS fk, COUNT(*) AS orphan_cnt
FROM ods_order_items i
LEFT JOIN ods_orders o USING (order_id)
WHERE o.order_id IS NULL

UNION ALL
SELECT '明细→商品', COUNT(*)
FROM ods_order_items i
LEFT JOIN ods_products p USING (product_id)
WHERE p.product_id IS NULL

UNION ALL
SELECT '明细→商家', COUNT(*)
FROM ods_order_items i
LEFT JOIN ods_sellers s USING (seller_id)
WHERE s.seller_id IS NULL

UNION ALL
SELECT '支付→订单', COUNT(*)
FROM ods_payments p
LEFT JOIN ods_orders o USING (order_id)
WHERE o.order_id IS NULL

UNION ALL
SELECT '评价→订单', COUNT(*)
FROM ods_order_reviews r
LEFT JOIN ods_orders o USING (order_id)
WHERE o.order_id IS NULL;

五项都返回 0。外键检查不通过时,先判断是源系统不一致还是采集丢数,不要直接删明细。

3.3 金额非负与金额对账

SELECT
    COUNT(*) FILTER (WHERE price < 0)             AS negative_price_cnt,
    COUNT(*) FILTER (WHERE freight_value < 0)     AS negative_freight_cnt,
    COUNT(*) FILTER (WHERE freight_value = 0)     AS free_freight_cnt,
    COUNT(*) FILTER (WHERE payment_value < 0)     AS negative_pay_cnt
FROM (
    SELECT price, freight_value, NULL AS payment_value FROM ods_order_items
    UNION ALL
    SELECT NULL, NULL, payment_value FROM ods_payments
) t;

非负检查应全部为 0;运费为 0 是包邮,不是错误。

支付与明细的金额对账:

WITH item_total AS (
    SELECT i.order_id, SUM(i.price + i.freight_value) AS amt
    FROM ods_order_items i
    JOIN ods_orders o USING (order_id)
    WHERE o.order_status NOT IN ('canceled', 'unavailable')
    GROUP BY i.order_id
),
pay_total AS (
    SELECT order_id, SUM(payment_value) AS amt
    FROM ods_payments
    GROUP BY order_id
)
SELECT COUNT(*) AS mismatch_cnt,
       ROUND(SUM(p.amt - i.amt), 2) AS net_diff
FROM item_total i
JOIN pay_total p USING (order_id)
WHERE ABS(p.amt - i.amt) > 0.01;

同一份数据,只算有效订单和把取消/不可用订单也算进来,会得到不同的差异单数和净差。报数字前先说口径,否则两个都对、却永远对不上。逐单差异应披露给业务,不要直接改订单金额。

3.4 日期合理

SELECT
    COUNT(*) FILTER (WHERE order_approved_at < order_purchase_timestamp)          AS approved_before_purchase,
    COUNT(*) FILTER (WHERE order_delivered_carrier_date < order_purchase_timestamp) AS carrier_before_purchase,
    COUNT(*) FILTER (WHERE order_delivered_customer_date < order_delivered_carrier_date) AS delivered_before_carrier,
    COUNT(*) FILTER (WHERE order_status = 'delivered'
                       AND order_delivered_customer_date IS NULL)                  AS delivered_missing_date
FROM ods_orders;

正常情况下这四项都应为 0。空值不参与比较,所以“有效订单但还没签收”不会被算成时间倒挂。

评价与签收的时间顺序:

SELECT COUNT(*) AS review_before_delivery
FROM ods_order_reviews r
JOIN ods_orders o USING (order_id)
WHERE o.order_delivered_customer_date IS NOT NULL
  AND r.review_creation_date < o.order_delivered_customer_date;

返回评价早于签收的记录数。这类记录可以保留,但分析“迟到与评分关系”时只按订单聚合 review_score,不要用评价时间做因果推断,避免把提前评价当成签收后的反馈。

3.5 枚举值合法

SELECT order_status, COUNT(*) AS cnt
FROM ods_orders
WHERE order_status NOT IN (
    'approved', 'canceled', 'created', 'delivered',
    'invoiced', 'processing', 'shipped', 'unavailable'
)
GROUP BY order_status;

预期 0 行。支付方式同理:

SELECT payment_type, COUNT(*) AS cnt
FROM ods_payments
WHERE payment_type NOT IN (
    'credit_card', 'debit_card', 'voucher', 'balance', 'unknown'
)
GROUP BY payment_type;

也返回 0。unknown 本身在允许列表里,但它出现时就意味着支付方式缺失,报表要单独统计。

3.6 波动监控

行数波动用“当天值”对比“过去 7 天均值”:

WITH daily AS (
    SELECT order_date AS dt, COUNT(DISTINCT order_id) AS order_cnt
    FROM dwd_order
    WHERE order_date >= DATE '2024-01-01'
      AND order_date < DATE '2024-02-01'
    GROUP BY order_date
),
base AS (
    SELECT dt, order_cnt,
           AVG(order_cnt) OVER (ORDER BY dt
                                ROWS BETWEEN 7 PRECEDING AND 1 PRECEDING) AS avg_7d
    FROM daily
)
SELECT dt, order_cnt, ROUND(avg_7d, 1) AS avg_7d
FROM base
WHERE avg_7d IS NOT NULL
  AND (order_cnt < avg_7d * 0.7 OR order_cnt > avg_7d * 1.3);

阈值 30% 只是示例,要按业务波动调整。大促期间订单暴涨属于正常,需要维护“预期波动日历”,否则每天都会误报。另外,日期边界要写死为数据范围:上界用 < 数据最后一天 + 1(左闭右开),不要写成未来年份,否则不存在的日期也会被当成正常数据。

4. 异常处理:丢弃、填充、告警、隔离

发现脏数据只是第一步,怎么处理决定了下游能不能信任数仓。

处理方式 适用情况 示例 风险
丢弃 确定无业务价值且可重算 完全重复的日志按主键去重 丢错后不可恢复
填充 有明确默认值 缺品类补 unknown 掩盖问题
告警 不能自动判断 支付差异、取消单有支付 需要人处理
隔离 数据可疑但需留证 时间倒挂、送达无签收时间 需要补数流程

4.1 丢弃

丢弃只用于规则明确、且原始数据仍在 ODS 可回溯的情况,例如完全重复的日志按主键去重。对重复的 review_id 不要做简单行级去重:同一条评价可能关联不同订单,直接保留一行会丢信息;正确做法是在 DWD 按 order_id 聚合评分。不要静默丢弃,要记录被丢弃的行数和原因。

4.2 填充

缺品类补 unknown 是合理填充,因为“未知”本身就是一个可解释的分类。但订单没有明细不能凭空补金额,否则 GMV 会被伪造。填充的前提是默认值有业务含义。

4.3 告警

以下情况适合告警而不是自动处理:

  • 取消或不可用订单出现支付(可能是退款或分期)。
  • 支付总额与明细差异超过阈值。
  • 状态为 delivered 却没有签收时间。
  • 当天订单行数比 7 天均值低 30%(与 3.6 的示例阈值一致,实际按业务调整)。
  • 分区到 SLA 时间还没生成。

4.4 隔离

隔离表保存原始行、错误类型、发现时间,供修复后重放:

CREATE TABLE dq_order_time_error (
    batch_date        DATE,
    error_type        STRING,
    order_id          STRING,
    purchase_at       TIMESTAMP,
    carrier_at        TIMESTAMP,
    delivered_at      TIMESTAMP,
    detail            STRING,
    created_at        TIMESTAMP
);

写入时用 INSERT INTO ... SELECT 把承运早于下单、签收早于承运的行原样搬进去,标记 error_type = 'time_inversion'。下游只读 DWD,不读隔离表;修复源数据后按批次重跑对应分区,再把隔离记录标记为已处理。

4.5 常见脏数据的处理决定

问题 处理 是否影响 GMV
评价 ID 重复 保留原始行,DWD 按订单聚合均分 否,影响评分与评价数
取消单有支付 保留并告警,不判错 否,GMV 已排除取消单
支付-明细差异 记录差异,不改订单金额 否,GMV 以明细为准
承运早于下单 隔离时间字段,履约指标排除 否,影响履约时长
签收早于承运 同上 否,影响履约时长
送达但无签收时间 告警并人工核对 否,影响履约完成率
评价早于签收 标记,不用于因果分析 否,影响评分解读
商品缺品类 补 unknown 并单列 否,影响品类占比
订单无明细 保留订单,明细指标排除 是,订单口径与明细口径不一致

提示:订单没有明细不必从“有效订单数”里删掉,因为订单确实存在;但算 AOV、品类占比时必须用有明细的有效订单。同一份数据,不同指标不同口径,这正是 DWD 宽表和 ADS 层要分别对账的原因。

5. 数据治理的五个抓手

数据质量是治理的一部分。治理解决的是“长期有人负责、规则可查、成本可控”。

5.1 元数据

元数据分两类:

  • 技术元数据:表结构、字段类型、分区、存储格式、文件大小、任务依赖。
  • 业务元数据:指标定义、负责人、更新频率、敏感级别、使用说明。

项目线里的数据说明记录来源、许可、字段字典和重下命令,指标基线则用机器可读的文件保存,两者都属于最基础的元数据。

5.2 血缘

血缘描述数据从哪里来、经过哪些任务、流到哪里去:

ods_orders ─┐
ods_order_items ─┼→ dwd_order_detail → dws_category_1m → ads_category_rank
ods_products ─┘

血缘的用途:上游改字段时评估影响范围;报表出错时快速定位;下线表时确认没有下游依赖。

5.3 数据安全与 PII

PII(Personally Identifiable Information,个人可识别信息)包括手机号、身份证号、银行卡号、详细地址、邮箱等。数据开发每天都在接触这些字段,安全不是“安全部门的事”。

四条基本规则:

  • 最小权限:按角色授权,只给完成工作必需的库表;不按个人堆权限,离职及时回收。
  • 列级脱敏:敏感字段在 ADS 层对外前脱敏,例如手机号只保留后四位,身份证只保留前六位和后四位。
  • 环境隔离:测试环境用脱敏数据,不直接复制生产库;开发同学不拿生产全量数据做实验。
  • 可审计:生产数据访问要审批、留日志;导出数据要登记用途和保留期限。

脱敏 SQL 示例(示意写法,假设源表有 phone、id_card 字段;示例数据没有这两个字段):

SELECT customer_id,
       CONCAT(SUBSTR(phone, 1, 3), '****', SUBSTR(phone, 8, 4)) AS phone_masked,
       CONCAT(SUBSTR(id_card, 1, 6), '********', SUBSTR(id_card, 15, 4)) AS id_masked
FROM ods_customers;

分层要求:

层 能否存原始 PII 说明
ODS 可以,但严格权限 贴源保留,只对少数开发开放
DWD 按需保留 能脱敏就先脱敏,保留业务主键
DWS 一般不保留 聚合到用户/品类粒度,不需要手机号
ADS 必须脱敏 面向报表和接口,只给必要的展示字段

面试怎么答:先讲“最小权限 + 列级脱敏 + 环境隔离 + 审计”,再举一个具体例子,例如“报表只展示手机号后四位,明细表不向分析同学开放”。不要只说“我们会注意安全”。

注意:脱敏不是简单地把字段删掉。很多指标需要用户唯一标识,正确做法是保留哈希后的 user_key,既能关联又无法反推原值。

5.4 成本

  • 存储:设置生命周期,ODS 原始数据按合规要求保留,临时表定期清理。
  • 计算:避免全表扫描,分区裁剪和列裁剪必须生效。
  • 小文件:过多小文件会增加 NameNode 压力和任务启动开销。
  • 重复建设:同一指标不要在多个 ADS 表里各算一遍。

5.5 SLA

SLA 是数据交付承诺,例如“每天 07:00 前产出昨日 GMV”。要配套:

  • 任务完成时间监控。
  • 失败重试和告警。
  • 值班人和升级路径。
  • 核心表与非核心表分级,资源优先保障核心链路。

6. 把体检结果落成质量报告

6.1 质量报告表

CREATE TABLE dq_check_result (
    check_date  DATE,
    table_name  STRING,
    check_name  STRING,
    rule_desc   STRING,
    error_cnt   BIGINT,
    status      STRING,
    owner       STRING
);

每次跑完检查,把结果写一行。status 用 pass、warn、fail 三档,看板按表和日期展示趋势。

6.2 清洗后的对账基线

指标 示例数值
原始订单 12
有效订单(排除 canceled / unavailable) 9
有有效明细的订单 9
原始明细 12
清洗后有效明细 9
GMV(含运费) 1,509.00
GMV(不含运费) 1,250.00
AOV 167.67

每次重跑后,行数和 GMV 必须与项目维护的指标基线一致。若不一致,先查清洗规则是否被改动,再查源数据是否变化。

6.3 一次完整的质量巡检流程

1. 检查 ODS 文件是否到齐、行数和 sha256 是否匹配
2. 检查主键唯一、外键完整(唯一性、一致性)
3. 检查金额、时间顺序、枚举(准确性)
4. 检查空值率和维度覆盖(完整性)
5. 对比昨日行数和 GMV(波动监控)
6. 生成质量报告和 metrics.json,失败项告警
7. 异常数据进隔离表,修复后按分区重跑
8. 用基线数字对账,确认下游恢复

7. 常见坑

  1. 把业务空值当脏数据:未送达订单没有签收时间是正常的,全部删除会丢单。
  2. 只做一次体检:上线后不监控,源系统变了也不知道。
  3. 静默丢弃:不记录丢弃行数和原因,出了问题无法追溯。
  4. 报差异不说口径:有效订单口径和全部订单口径的差异单数不同,混着说就永远对不上。
  5. 告警阈值拍脑袋:不区分大促和日常,告警很快会被忽略。
  6. 只查单表不查跨表:明细行数全对,但支付金额和明细对不上,报表照样错。
  7. 把包邮当异常:运费为 0 是合法业务,不能按 freight <= 0 过滤。

8. 在线练习

题目 考点
196. 删除重复的电子邮箱 重复数据识别
1173. 即时食物配送 I 🔒 比例指标
1084. 销售分析 III 时间范围校验

自测:

  • 能把“评价重复、支付差异、时间倒挂、无明细订单、缺品类”分别归到五个维度。
  • 能写出空值率和主键重复检查 SQL。
  • 能区分“错误、告警、信息”三级,并说明哪一级应该阻断构建。
  • 能解释支付与明细的差异为什么只披露、不直接修改订单金额。

9. 面试延伸

面试题:你怎么保证数据质量?

是什么:数据质量要覆盖完整性、唯一性、一致性、准确性、及时性五个维度,并贯穿入仓前、加工中、上线后三个阶段。

解决什么问题:入仓前用探查 SQL 摸清脏数据;DWD 层做硬性校验,异常数据隔离而不是静默丢弃;上线后配监控告警,检查主键、外键、行数波动和核心指标,发现问题按分层逐级回溯。

我的实操边界:项目里我处理过评价去重、支付与明细对账、时间倒挂、缺品类、无明细订单 5 类问题,并做过行数和 GMV 对账;没有在生产环境搭建过完整的质量平台。

面试题:数据治理具体做什么?

是什么:数据治理包括元数据、血缘、权限、成本、SLA 和质量管理。

解决什么问题:让数据可发现、可追溯、可授权、可控制成本、可承诺交付。没有治理,数仓会变成“表很多、没人知道哪张能用”的数据沼泽。

我的实操边界:项目里维护了字段字典和分层血缘,做过简单的质量报告表设计;没有参与过公司级权限体系和成本治理。

面试题:数据延迟了怎么办?

是什么:延迟是指数据没有在 SLA 时间内到达,可能来自上游采集、计算任务或调度依赖。

解决什么问题:先看分区和任务状态定位卡点,再评估是否用上一日数据兜底,同时通知下游;恢复后按分区补数,确保幂等。核心报表要有降级方案,不能因为一张表延迟让整个看板空白。

我的实操边界:我理解补数和幂等的原理,项目用全量重跑保证一致性;没有在生产调度系统里处理过跨天延迟。

10. 本章验收

  • 能说出数据质量五个维度,并各举一个例子
  • 能默写主键唯一、外键完整、金额非负、日期合理、枚举合法、波动监控 6 类 SQL
  • 能区分业务空值和脏数据,正确解释“未签收所以签收时间为空”
  • 能针对时间倒挂设计“隔离、过滤、修复、重放”的完整流程
  • 能说出元数据、血缘、权限、成本、SLA 各自解决什么问题
  • 能背出示例数据清洗后的对账基线:9 个有效订单、1,509.00 GMV、167.67 AOV
本章任务0 / 10
本页目录