订阅更新

DWD层清洗了什么?面试官说你只会去重和过滤——清洗规则怎么管理?

数仓项目手册 第 9 题 约 10 分钟读完

本文目录
  1. 先说结论
  2. DWD八大清洗动作
  3. 完整的DWD清洗SQL示例
  4. 清洗规则配置化
  5. 面试高频追问
  6. 实战踩坑
  7. 总结

先说结论

DWD层清洗的本质不是'删坏数据',而是'让数据变成可信赖的分析原材料'。清洗的完整动作包括:去重、空值处理、格式标准化、字段映射、维度退化、类型转换、异常过滤、数据脱敏——至少8个环节。

清洗规则不能硬编码在SQL里。要做到规则配置化——清洗规则写在配置表或配置文件里,SQL通过参数引用规则,改规则不需要改代码。

面试加分关键:能说出'清洗了什么'只是基础分,能说出'清洗规则怎么管理、怎么保证上下游一致'才是高分。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

很多人在面试中被问到'你的DWD层做了什么清洗',回答就是'去重、过滤空值'——然后面试官追问'还有呢',就卡住了。DWD层的清洗远不止去重和过滤,它是数据质量的第一道防线。我在某短剧出海公司负责DWD层的建设,整理了一套完整的清洗规则体系,包括8大清洗动作和规则配置化管理方案。

DWD八大清洗动作

清洗动作说明示例优先级
① 去重相同event_id的重复数据只保留一条Kafka At-Least-Once导致重复必做
② 空值处理关键字段为空时填充默认值或标记user_id为空填'unknown'必做
③ 格式标准化字段值统一格式(大小写、编码等)channel统一大写:facebook→FACEBOOK必做
④ 字段映射原始编码转为可读含义pay_type: 1→'credit_card', 2→'paypal'推荐
⑤ 维度退化关联维度表将维度信息退化到事实表关联country_code→country_name推荐
⑥ 类型转换字符串转为正确的数据类型金额从STRING转DECIMAL必做
⑦ 异常过滤过滤不合理的数据金额为负、时间在1970年必做
⑧ 数据脱敏敏感信息加密或脱敏手机号中间4位替换为****按合规要求

完整的DWD清洗SQL示例

以短剧出海公司的用户付费事件DWD为例,展示所有8个清洗动作的具体实现:

-- DWD用户付费事件清洗SQL(8个清洗动作全覆盖)
INSERT OVERWRITE TABLE dwd_user_payment_di PARTITION(dt='${bizdate}')
SELECT
    -- ① 去重:用ROW_NUMBER按event_id去重,保留最早一条
    t.event_id,
 
    -- ② 空值处理:user_id为空填'unknown'
    COALESCE(t.user_id, 'unknown') AS user_id,
 
    -- ③ 格式标准化:渠道名统一大写
    UPPER(t.channel) AS channel,
 
    -- ④ 字段映射:支付方式编码→可读含义
    CASE t.pay_type
        WHEN '1' THEN 'credit_card'
        WHEN '2' THEN 'paypal'
        WHEN '3' THEN 'google_pay'
        WHEN '4' THEN 'apple_pay'
        ELSE 'other'
    END AS pay_method,
 
    -- ⑤ 维度退化:关联国家维度表
    dim_country.country_name,
    dim_country.timezone,
    dim_country.currency AS local_currency,
 
    -- ⑥ 类型转换:金额从STRING转DECIMAL
    CAST(t.pay_amount AS DECIMAL(10,2)) AS pay_amount,
 
    -- ⑧ 数据脱敏:邮箱脱敏(保留@前2字符)
    CONCAT(SUBSTR(t.email, 1, 2), '****@',
        SPLIT(t.email, '@')[1]) AS email_masked,
 
    -- 时间标准化
    TO_TIMESTAMP(t.event_time, 'yyyy-MM-dd HH:mm:ss') AS event_ts,
    t.server_time AS server_ts
 
FROM (
    -- ① 去重子查询
    SELECT *, ROW_NUMBER() OVER(
        PARTITION BY event_id ORDER BY server_time ASC) AS rn
    FROM ods_user_payment_di
    WHERE dt = '${bizdate}'
      -- ⑦ 异常过滤
      AND event_id IS NOT NULL
      AND CAST(pay_amount AS DECIMAL(10,2)) > 0     -- 金额必须>0
      AND event_time >= '2020-01-01'                 -- 过滤异常早期时间
      AND event_time <= '${bizdate} 23:59:59'        -- 不能有未来时间
) t
LEFT JOIN dim_country ON t.country_code = dim_country.country_code
WHERE t.rn = 1;  -- 去重:只取第一条

清洗规则配置化

上面的SQL里,很多清洗规则(比如pay_type的映射关系、异常过滤的阈值)是硬编码的。一旦规则变了(比如新增了一种支付方式),就要改SQL、测试、上线。更好的做法是把规则配置化:

-- 方案:清洗规则放在配置表里,SQL动态引用
 
-- 配置表:dwd_clean_rules(存在MySQL/Hive中)
-- | rule_id | table_name       | field    | rule_type | rule_value      |
-- |---------|------------------|----------|-----------|-----------------|
-- | R001    | user_payment     | pay_type | mapping   | 1=credit_card   |
-- | R002    | user_payment     | pay_type | mapping   | 2=paypal        |
-- | R003    | user_payment     | amount   | range     | 0,999999        |
-- | R004    | user_payment     | email    | mask      | prefix:2,suffix |
 
-- DWD SQL中引用配置表做字段映射
SELECT
    t.event_id,
    COALESCE(mapping.mapped_value, 'other') AS pay_method
FROM ods_user_payment_di t
LEFT JOIN (
    -- 从配置表动态加载映射关系
    SELECT
        SPLIT(rule_value, '=')[0] AS original_value,
        SPLIT(rule_value, '=')[1] AS mapped_value
    FROM dwd_clean_rules
    WHERE table_name = 'user_payment'
      AND field = 'pay_type'
      AND rule_type = 'mapping'
) mapping ON t.pay_type = mapping.original_value;

配置化的价值:新增支付方式时,只需要在配置表INSERT一行,不需要改DWD的SQL。我们在某短剧出海公司,支付方式从4种增加到8种,全程没有修改DWD代码,只是在配置表加了4行数据。

面试高频追问

追问一:去重为什么用ROW_NUMBER而不是DISTINCT?

因为DISTINCT是按所有字段去重,但重复数据可能部分字段不同(比如两条相同event_id但server_time不同的记录)。ROW_NUMBER可以指定去重的key(event_id)和保留策略(保留server_time最早的一条),控制力更强。另外在Hive/MaxCompute里,DISTINCT会触发全数据Shuffle,而ROW_NUMBER配合PARTITION BY只在分区内排序,性能更好。

追问二:ODS层为什么不做清洗,非要多一层DWD?

核心原因是'可追溯'。ODS保留原始数据,如果DWD的清洗逻辑有bug,可以从ODS重跑修复。如果在ODS层就清洗了,原始数据就丢了,发现问题后只能找上游重推——而上游的数据可能已经过期被删除了。另一个原因是'关注点分离'——ODS只管安全落地,DWD专注数据质量,职责清晰,排查问题时知道该看哪一层。

追问三:清洗过程中被过滤掉的数据怎么处理?直接丢了吗?

不能直接丢。我们把被过滤的异常数据写入一张'垃圾桶表'(dwd_rejected_events),记录被过滤的原因(哪个规则命中的)。这些数据有两个用途:① 统计异常率——如果某天异常数据突然暴增,说明上游可能出了问题;② 排查数据丢失——如果业务方反馈某条数据查不到,可以先在垃圾桶表里找,看看是不是被清洗规则误杀了。

实战踩坑

在某短剧出海公司遇到过一次'清洗过度'的事故。我们的异常过滤规则有一条是'金额大于10000的订单视为异常过滤'——因为我们短剧的单笔消费一般在1-99美元之间。但有一天土耳其区域上线了'年度VIP'套餐,定价14999土耳其里拉(约500美元),折算成美元后低于10000,但我们的pay_amount字段存的是当地币种金额——14999里拉直接被过滤了。

结果土耳其区域当天的付费数据全部丢失,运营发现土耳其收入为0才开始排查。修复方案:① 紧急把过滤阈值从10000调到999999(临时放开);② 从垃圾桶表恢复被误杀的数据;③ 长期方案是改成按美元等值金额过滤——先做币种转换,再判断是否异常。

-- 踩坑:过滤规则没考虑多币种
 
-- 错误写法(直接用原始金额判断)
WHERE pay_amount > 0 AND pay_amount < 10000  -- ❌ 14999里拉被过滤了
 
-- 正确写法(先转换成美元,再判断)
WHERE pay_amount > 0
  AND pay_amount * exchange_rate < 10000      -- ✅ 14999 * 0.033 = 495美元,正常
 
-- 更好的写法(规则配置化,不同币种不同阈值)
WHERE pay_amount > 0
  AND pay_amount < (
    SELECT threshold FROM dwd_clean_rules
    WHERE table_name = 'user_payment'
      AND field = 'pay_amount'
      AND rule_type = 'max_threshold'
      AND currency = t.currency       -- 按币种取不同阈值
  )

总结

DWD层清洗的完整动作是8项:去重、空值处理、格式标准化、字段映射、维度退化、类型转换、异常过滤、数据脱敏。面试时不要只说'去重和过滤'——要能列出完整的清洗动作,并给每个动作举一个具体的业务例子。更高级的回答是讲清洗规则的配置化管理和异常数据的处理机制(垃圾桶表),这展示了你在数据治理方面的思考深度。

DWD清洗完了,下一步是DWS汇总层——汇总什么、按什么粒度汇总、怎么避免过度建设?下一篇聊DWS层的设计方法论。

你们DWD层有多少条清洗规则?有没有遇到过清洗规则误杀数据的情况?评论区聊聊~

关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长

刷题进度保存在当前浏览器