4. DWD层(明细数据层)
DWD层,全称是Data Warehouse Detail,明细数据层。很多刚入行的朋友会把它简单理解成「把ODS层的数据搬过来,稍微洗一洗」。说实话,我早年也这么干过,结果后面做汇总层的时候,各种数据对不上,排查问题查到怀疑人生。
DWD层的核心使命,就是做一件事:把业务数据变成分析可用的数据。它不负责汇总,不负责宽表,它只负责把数据洗干净、标准化、拆解好。你想想看,如果源头的数据都是脏的、乱的、口径不统一的,上层再怎么建模都是白搭。
DWD层的核心定位: 业务数据与分析模型之间的「翻译层」。它把OLTP系统的操作型数据,翻译成OLAP系统需要的分析型数据。
4.1 DWD层的定位与作用
我个人的习惯是,把DWD层看作数据仓库的「地基」。地基打不牢,楼盖得再高也得塌。具体来说,DWD层承担了三个关键角色:
- 数据清洗与标准化:去掉脏数据,统一编码、单位、时区、格式
- 业务过程建模:把业务事件拆解成原子粒度的事实和维度
- 维度退化:把冗余的描述信息从事实表中剥离,或者直接退化到事实表
我记得有一次做电商项目,ODS层直接同步了MySQL的订单表,里面有个字段叫order_status,值有'0'、'1'、'2'、'3',还有'待支付'、'已发货'、'已完成'。同一个字段,不同时期的数据,编码规则都不一样。这种数据如果不做清洗和标准化,直接拿去分析,结果可想而知。
注意: DWD层的数据粒度必须与ODS层保持一致,或者更细。不要在这里做任何聚合操作,否则会丢失明细信息,后续无法回溯。
4.2 数据清洗与标准化
数据清洗,说白了就是「去伪存真」。标准化,就是「统一口径」。这两件事在DWD层必须做彻底。
我一般把清洗规则分成四类:
| 清洗类型 | 常见问题 | 处理方式 |
|---|---|---|
| 完整性清洗 | 空值、缺失值 | 填充默认值、剔除、标记异常 |
| 准确性清洗 | 数据格式错误、越界值 | 格式校验、范围校验、规则修正 |
| 一致性清洗 | 编码不统一、单位不统一 | 映射转换、单位换算、时区对齐 |
| 时效性清洗 | 重复数据、过期数据 | 去重、时间戳过滤、版本管理 |
举个例子,订单金额的单位问题。有的系统存的是「分」,有的存的是「元」。如果不统一,汇总的时候就会差100倍。我建议在DWD层统一转换成「元」,保留两位小数,并且字段名加个注释-- 单位:元,方便后面的人理解。
小技巧: 清洗逻辑不要写在ETL脚本里,要写成独立的清洗函数或存储过程。这样当上游数据源变更时,只需要改清洗函数,不用动整个ETL流程。
4.3 维度退化
维度退化这个概念,很多人第一次听会觉得有点绕。我换个说法:把维度表中那些「一次性」的描述信息,直接塞回事实表里。
为什么会这样?你想想看,有些维度属性,比如订单的「支付方式」,它只跟这个订单本身有关,不会跟其他订单共享。如果单独建一张支付方式维度表,里面可能就两三条记录(支付宝、、银行卡),查询的时候还要多一次JOIN,得不偿失。
所以,我个人的做法是:
- 高基数、低共享的维度属性:直接退化到事实表,比如订单号、支付流水号、设备ID
- 低基数、高共享的维度属性:保留维度表,比如商品分类、地区、时间
- 中间情况:看查询频率,如果90%的查询都要用到这个属性,就退化
我曾经在一个金融项目中,把「交易渠道」这个字段退化到了事实表。原本设计了一张渠道维度表,有20多个字段。后来发现,每次分析交易数据都要关联这张表,查询性能很差。退化之后,事实表只保留了一个渠道编码字段,查询速度提升了3倍多。
维度退化的核心原则: 减少JOIN,提升查询性能。但不要过度退化,否则事实表会变得臃肿不堪。
4.4 事实表设计
事实表是DWD层的核心产出。三种类型的事实表,各有各的适用场景。我一个个说。
4.4.1 事务事实表
事务事实表,记录的是「每一笔业务事件」。比如每一笔订单、每一次登录、每一次点击。它的特点是:一行代表一个事件,一旦写入就不会修改。
适用场景:
- 需要分析单个事件的细节,比如订单金额、支付时间
- 需要统计事件发生的次数,比如日活、订单量
- 需要做事件间的关联分析,比如下单到支付的转化
设计要点:
- 粒度必须最细,不要合并同类事件
- 外键尽量使用代理键,不要直接用业务主键
- 度量字段必须是可加的,比如金额、数量
-- 事务事实表示例:订单事实表
CREATE TABLE dwd_order_fact (
order_sk BIGINT, -- 代理键
order_id VARCHAR(32), -- 退化维度:订单号
user_sk BIGINT, -- 用户维度外键
product_sk BIGINT, -- 商品维度外键
store_sk BIGINT, -- 门店维度外键
order_amount DECIMAL(10,2),-- 订单金额(元)
order_qty INT, -- 订单数量
order_time TIMESTAMP, -- 下单时间
pay_time TIMESTAMP, -- 支付时间
-- 其他退化维度
pay_channel VARCHAR(16), -- 支付渠道
order_status VARCHAR(8) -- 订单状态
);
4.4.2 周期快照事实表
周期快照事实表,记录的是「某个时间点的状态」。比如每天的用户余额、每月的库存量。它的特点是:按固定周期(每天、每月)记录快照,即使数据没变也会生成一条记录。
适用场景:
- 需要分析某个时间点的存量数据,比如账户余额、库存水位
- 需要做趋势分析,比如连续30天的用户活跃度变化
- 事务事实表无法直接计算的累积指标,比如月活跃用户数
设计要点:
- 必须包含快照时间字段,通常是日期维度外键
- 度量字段通常是半可加的,比如余额不能跨时间相加
- 数据量会随时间线性增长,需要做好分区策略
-- 周期快照事实表示例:每日用户余额快照
CREATE TABLE dwd_user_balance_snapshot (
snapshot_date DATE, -- 快照日期
user_sk BIGINT, -- 用户维度外键
balance DECIMAL(10,2),-- 当日余额
frozen_amount DECIMAL(10,2),-- 冻结金额
total_deposit DECIMAL(10,2),-- 累计充值(截至当日)
update_time TIMESTAMP -- 最后更新时间
);
经验之谈: 周期快照事实表的数据量通常很大,建议按日期做分区,并且只保留最近N个月的数据。历史数据可以归档到冷存储。
4.4.3 累积快照事实表
累积快照事实表,记录的是「一个业务流程从开始到结束的完整过程」。比如一笔订单从下单、支付、发货到签收的全生命周期。它的特点是:一行代表一个业务流程实例,随着流程推进不断更新。
适用场景:
- 需要分析业务流程的时长,比如从下单到签收的平均时间
- 需要追踪业务流程的进度,比如当前有多少订单在「已发货」状态
- 需要做漏斗分析,比如从下单到支付的转化率
设计要点:
- 包含多个时间戳字段,记录每个关键节点的时间
- 包含状态字段,记录当前所处的流程阶段
- 数据行会更新,直到流程结束
-- 累积快照事实表示例:订单生命周期事实表
CREATE TABLE dwd_order_lifecycle (
order_sk BIGINT, -- 代理键
order_id VARCHAR(32), -- 订单号
create_time TIMESTAMP, -- 下单时间
pay_time TIMESTAMP, -- 支付时间
ship_time TIMESTAMP, -- 发货时间
receive_time TIMESTAMP, -- 签收时间
cancel_time TIMESTAMP, -- 取消时间(如果有)
current_status VARCHAR(16), -- 当前状态:已下单/已支付/已发货/已签收/已取消
total_duration INT, -- 总耗时(分钟),流程结束后计算
-- 维度外键
user_sk BIGINT,
product_sk BIGINT,
store_sk BIGINT
);
注意: 累积快照事实表是「可变」的,ETL过程中需要做更新操作。如果数据量很大,更新性能会成为瓶颈。建议配合拉链表或增量更新策略使用。
4.5 三种事实表对比
为了方便你理解,我把三种事实表的差异整理成了一张表:
| 对比维度 | 事务事实表 | 周期快照事实表 | 累积快照事实表 |
|---|---|---|---|
| 记录内容 | 单笔事件 | 时间点状态 | 流程全貌 |
| 数据更新 | 只追加,不更新 | 追加新快照 | 更新已有行 |
| 粒度 | 每笔事件一行 | 每个周期每个实体一行 | 每个流程实例一行 |
| 典型场景 | 订单、点击、支付 | 余额、库存、日活 | 订单生命周期、工单流程 |
| 度量可加性 | 完全可加 | 半可加(跨时间不可加) | 半可加(跨实例不可加) |
4.6 DWD层核心知识体系
下面这张图,是我梳理的DWD层核心知识结构。你可以把它当作一个检查清单,看看自己的DWD层设计是否覆盖了这些要点。
嗯,DWD层的内容就讲到这里。说白了,它就是数据仓库里最「脏」最「累」的活,但也是最有价值的一层。把DWD层做好了,后面的DWS、ADS层就是水到渠成的事。