1. 数据分层不是概念游戏,是数据仓库的骨架
刚入行做数据那会儿,我最烦听到的就是“分层”两个字。总觉得这是架构师们为了画架构图好看,硬生生造出来的一堆缩写——ODS、DW、DM,听着就头大。直到有一次,我接手了一个“数据中台”项目,上游业务库有几十张表,下游报表需求上百个,每天跑批时间窗口只有四个小时。那时候我才真正理解,分层不是画着玩的,它是数据仓库的骨架,是让整个数据流从一团乱麻变成有序管道的唯一办法。
数据分层,说白了就是给数据从“生”到“用”的过程划分出不同的加工车间。原始数据从业务系统抽过来,先放进一个“原料仓库”,这就是ODS(Operational Data Store,操作数据存储)。原料仓库里的东西基本不动,保持和业务系统一样的模样,方便追溯和核对。然后,这些原料被送到“加工车间”进行清洗、整合、关联,变成干净、规范、有主题的中间层,这就是DW(Data Warehouse,数据仓库)。最后,加工好的半成品或成品被送到“成品展厅”,按照不同的业务需求组装成报表、标签、指标,直接给业务方看,这就是DM(Data Mart,数据集市)。
这套逻辑听起来简单,但真正落地的时候,坑一个接一个。比如,ODS 层到底要不要做清洗?DW 层怎么划分主题域?DM 层是每个业务线一个还是统一一个?这些问题没有标准答案,只有适合当前团队和业务阶段的答案。我见过太多团队,要么分层太细,维护成本高到离谱;要么分层太粗,ODS 直接对接报表,改一个字段牵动全身。所以,这篇文章我想从实际操盘的角度,把 ODS、DW、DM 这三层到底怎么分、怎么建、怎么避坑,掰开揉碎了讲清楚。不管你是刚接触数据仓库的新人,还是正在重构数据链路的老人,应该都能找到一些能直接抄作业的东西。
2. ODS 层:别急着洗干净,先原汁原味存下来
2.1 ODS 的核心定位是“历史快照+数据溯源”
很多人一上来就想在 ODS 层做数据清洗,把脏数据、空值、格式问题全处理掉。我的建议是:千万别。ODS 层的核心价值在于“还原”,它要能回答“昨天下午三点这笔订单在业务系统里长什么样”这种问题。如果你在 ODS 层就把数据改了,一旦下游报表对不上,你连排查的原始依据都没有。
我一般会把 ODS 层设计成“贴源”结构,也就是和业务库表结构基本保持一致,只做极少的标准化处理,比如统一字段命名风格、加上etl_time这样的抽取时间戳。这样做的好处是,任何数据问题都能回溯到 ODS 层,和业务方对账的时候,直接拿 ODS 的数据和业务系统比,一目了然。
注意:ODS 层的数据保留周期要根据业务合规要求和存储成本来定。一般建议至少保留 3 到 6 个月,金融、医疗等强监管行业可能需要保留更久。
2.2 抽取策略:全量还是增量,这是个问题
ODS 层的数据怎么从业务库抽过来,是第一个要做的技术决策。全量抽取简单粗暴,每天把整张表拉一遍,但数据量大了之后,对业务库和网络的压力都很大。增量抽取效率高,但需要业务表有可靠的时间戳或自增 ID,而且处理更新和删除操作时容易出问题。
我自己的经验是,小表全量,大表增量,核心表做拉链。比如配置表、维度表这种几千几万行的,直接全量覆盖,省事又不容易错。订单、流水这种每天几百万上千万的,必须走增量,一般用update_time作为水位线。但这里有个坑:业务系统里的update_time不一定可靠,有些批量操作不会更新这个字段,导致增量抽取漏数据。所以,我通常会配合源库的 binlog 或者 CDC 工具来做兜底,确保数据不丢。
-- 一个典型的增量抽取 SQL 示例 SELECT * FROM source_order WHERE update_time > '${last_extract_time}' AND update_time <= '${current_extract_time}';这个 SQL 看起来简单,但${last_extract_time}的维护很关键。我一般会在 ODS 层建一张etl_watermark表,记录每张源表上次抽取的最大时间戳,每次跑批先读水位线,抽完再更新。这样即使任务失败重跑,也不会重复抽数据。
2.3 ODS 层的分区与存储优化
ODS 层的数据量往往是整个仓库里最大的,所以分区设计直接影响到查询性能和存储成本。我通常按日期做一级分区,因为绝大多数抽取任务都是按天调度的。如果单日数据量特别大,比如超过 1 亿行,可以考虑再按业务线或地区做二级分区。
存储格式上,如果用的是 Hive 或 Spark 生态,我强烈建议用ORC 或 Parquet这种列式存储,压缩比高,查询时只读需要的列,速度比 TextFile 快好几倍。如果用的是关系型数据库做 ODS,那就要注意索引的设计,但索引太多又会影响写入性能,需要根据查询模式权衡。
| 存储格式 | 压缩比 | 查询速度 | 写入速度 | 适用场景 |
|---|---|---|---|---|
| TextFile | 低 | 慢 | 快 | 临时数据、调试 |
| ORC | 高 | 快 | 中 | 大规模数仓、分析场景 |
| Parquet | 高 | 快 | 中 | 嵌套数据结构、Spark 生态 |
| Avro | 中 | 中 | 快 | 行级写入、流式场景 |
3. DW 层:从“原料”到“半成品”的加工车间
3.1 主题域划分:别按部门分,按业务过程分
DW 层是整个数据仓库的核心,也是最容易做歪的地方。我见过不少团队,直接按业务部门来划分主题域,比如“销售域”、“财务域”、“运营域”。这种分法看起来清晰,但实际上后患无穷。因为业务部门是会变的,组织架构一调整,主题域就得跟着改,下游所有任务都要动。
更合理的做法是按业务过程来划分主题域。比如电商场景,核心业务过程就是“下单”、“支付”、“发货”、“退款”、“评价”。每个业务过程对应一个事实表,围绕这个事实表再去关联相关的维度,比如用户、商品、门店、时间。这样划分出来的主题域是稳定的,不会因为组织架构调整而改变。
我一般会把 DW 层再细分为DWD(数据仓库明细层)和DWS(数据仓库汇总层)。DWD 层保持和 ODS 层一样的粒度,但做了清洗、规范化、维度退化等处理,相当于“精加工后的明细”。DWS 层则按主题进行轻度汇总,比如“用户日粒度消费汇总”、“商品日粒度销售汇总”,为上层 DM 提供更高效的查询基础。
3.2 维度建模:星型模型还是雪花模型
维度建模是 DW 层的核心方法论,最常用的就是星型模型和雪花模型。星型模型以一个事实表为中心,周围直接关联多个维度表,维度表不再规范化,结构简单,查询性能好。雪花模型则是对维度表进一步规范化,拆成多个层级,节省存储但查询时 Join 更多,性能会下降。
我的建议是:优先用星型模型。现在存储成本已经很低了,没必要为了省那点空间去牺牲查询性能。而且星型模型对业务人员更友好,他们看表结构就能理解数据关系。只有在维度特别大、冗余特别严重的情况下,才考虑雪花模型。
提示:维度表的主键设计很关键。我一般会用代理键(surrogate key),而不是业务主键。代理键用自增整数或哈希值,避免业务主键变更导致的历史数据关联断裂。
3.3 缓慢变化维:Type 1 还是 Type 2
缓慢变化维(SCD)是维度建模里绕不开的话题。最简单的 Type 1 是直接覆盖,不保留历史。Type 2 是拉链式,每次变化新增一条记录,用start_date和end_date标记有效期。还有 Type 3 是加一列“前值”,但用得比较少。
我的经验是,核心维度用 Type 2,普通维度用 Type 1。比如用户维度,用户的等级、城市、手机号可能会变,如果业务方需要分析“用户在下单时的等级”,那就必须用 Type 2,保留历史快照。而像商品分类这种,如果业务方不关心历史分类,直接用 Type 1 覆盖就行,维护成本低很多。
-- Type 2 缓慢变化维的更新逻辑示例 -- 当检测到维度属性变化时 UPDATE dim_user SET end_date = '${current_date}', is_current = 0 WHERE user_id = '${user_id}' AND is_current = 1; INSERT INTO dim_user (user_id, user_level, city, start_date, end_date, is_current) VALUES ('${user_id}', '${new_level}', '${new_city}', '${current_date}', '9999-12-31', 1);这个逻辑看起来简单,但在实际跑批时要注意并发更新的问题。如果多个任务同时更新同一个维度,可能会产生重复的当前记录。我一般会给维度表加一个唯一约束,或者用事务保证原子性。
4. DM 层:让业务方看得懂、查得快
4.1 DM 层的本质是“面向场景的预计算”
DM 层是直接面向业务应用的,所以它的设计原则只有一个:怎么让业务方用得爽。业务方不关心你的数据怎么分层、怎么建模,他们只关心“我要的指标能不能快速查出来”、“报表能不能按时出”。
所以,DM 层的核心工作就是预计算。把业务方常用的指标、维度组合,提前在 DW 层的基础上算好,存成宽表或者聚合表。比如“每日各品类销售额”、“每月各渠道新增用户数”,这些指标如果每次都从 DWD 层现算,跑批时间会很长。提前算好放在 DM 层,报表查询就是简单的SELECT,秒级返回。
我一般会把 DM 层分成两类:指标型 DM和标签型 DM。指标型 DM 主要存聚合指标,比如销售额、订单量、转化率,按不同维度组合预计算。标签型 DM 主要存用户标签、商品标签,比如“高价值用户”、“潜在流失用户”,供推荐系统或营销系统调用。
4.2 宽表还是聚合表:根据查询模式来定
DM 层到底是做成宽表还是聚合表,取决于业务方的查询模式。如果业务方经常查明细,比如“某个订单的详情”,那就需要宽表,把订单相关的用户、商品、支付信息都打平到一张表里。如果业务方主要看汇总数据,比如“本月销售额”,那就用聚合表,按维度组合预计算。
我自己的做法是两者都做,但控制数量。宽表不要超过 20 张,聚合表不要超过 50 张,否则维护成本会失控。每张表都要有明确的业务方和查询场景,没有明确使用者的表坚决不建。
| DM 表类型 | 粒度 | 查询模式 | 典型场景 |
|---|---|---|---|
| 明细宽表 | 订单/用户/商品 | 点查、详情 | 订单详情、用户画像 |
| 聚合表 | 日/月/品类/渠道 | 汇总、趋势 | 日报、月报、看板 |
| 标签表 | 用户/商品 | 圈选、推送 | 精准营销、推荐 |
4.3 DM 层的刷新策略:全量还是增量
DM 层的刷新策略也很讲究。全量刷新简单,每天把整张表重算一遍,但数据量大时跑批时间会很长。增量刷新只算变化的部分,效率高,但逻辑复杂,容易出错。
我的经验是:小表全量,大表增量,指标表按分区覆盖。比如标签表,用户量几百万,全量刷新也就几分钟,那就全量。订单聚合表,每天几千万行,那就按日期分区,每天只覆盖当天的分区。这样即使某天任务失败,重跑也只影响当天,不会把历史数据搞乱。
注意:增量刷新一定要处理好迟到数据。比如订单支付后可能隔几天才退款,如果 DM 层只算当天的数据,退款金额就统计不到。我一般会在 DM 层保留最近 7 到 30 天的滚动窗口,每天重算这个窗口内的数据,确保迟到数据能被修正。
5. 分层落地时最容易踩的五个坑
5.1 坑一:ODS 层直接对接报表
这是最常见的错误。有些团队为了省事,报表直接查 ODS 层的数据。短期看没问题,但时间一长,业务方要加个指标、改个口径,你就得去改 ODS 的抽取逻辑,而 ODS 是贴源的,改了就破坏了“还原”的原则。更严重的是,ODS 层没有清洗和整合,不同业务系统的同名字段含义可能完全不同,报表数据很容易出错。
正确的做法是,任何报表都必须经过 DW 或 DM 层。ODS 只负责存储原始数据,不承担任何分析职责。这样即使业务系统升级、表结构变更,也只需要调整 ODS 到 DW 的加工逻辑,不会影响下游报表。
5.2 坑二:DW 层主题域划分过细
有些团队把主题域划分得特别细,比如“销售域”下面再分“线上销售”、“线下销售”、“海外销售”,每个子域一套 DWD 和 DWS 表。结果就是表数量爆炸,维护成本极高,而且很多表的数据量很小,跑批效率反而下降。
我的建议是,主题域划分要适度。一般中小型数仓,5 到 8 个主题域就够了。每个主题域下面,DWD 层按业务过程建表,DWS 层按分析维度建表。不要为了“看起来清晰”而过度拆分。
5.3 坑三:DM 层表没有明确的生命周期
DM 层的表如果只增不减,几年下来就是几百上千张表,没人知道哪些还在用、哪些可以删。我见过一个团队,DM 层有 800 多张表,实际每天被查询的不到 100 张,剩下的全是“僵尸表”,占着存储和计算资源。
我的做法是,每张 DM 表都要有负责人和过期时间。建表的时候就要登记:谁用、用来做什么、预计使用多久。每季度做一次盘点,超过 3 个月没有查询记录的表,直接下线。这样能保持 DM 层的精简和高效。
5.4 坑四:忽略数据质量监控
分层架构建好了,但如果数据质量没有监控,那就是“金玉其外,败絮其中”。我经历过一次事故,ODS 层抽取任务失败,但下游 DW 和 DM 层没有感知,继续用旧数据跑,结果报表出了三天的错误数据,业务方拿着报表去开会,闹了大笑话。
从那以后,我在每一层都加了数据质量监控。ODS 层监控抽取行数、主键唯一性;DW 层监控关联率、空值率;DM 层监控指标波动。一旦发现异常,立即告警并阻断下游任务。这套机制虽然增加了开发量,但避免了无数次数据事故。
5.5 坑五:元数据管理缺失
分层架构里,表与表之间的依赖关系非常复杂。如果没有元数据管理,你根本不知道改一张 ODS 表会影响哪些 DW 和 DM 表。我见过一个团队,因为改了一个 ODS 字段的类型,导致下游几十张表跑批失败,排查了一整天才找到原因。
所以,元数据管理必须从第一天就做。至少要有表级和字段级的血缘关系,能回答“这张表的数据从哪来”、“这张表被哪些表用了”。现在有很多开源工具可以做这件事,比如 Atlas、DataHub,选一个适合自己技术栈的,尽早接入。
6. 一套可复用的分层规范模板
6.1 命名规范:让表名自己说话
命名规范是分层落地的第一步。我一般会用这样的格式:{层级}_{主题域}_{业务过程}_{刷新频率}。比如dwd_trade_order_di表示“数据仓库明细层_交易域_订单_日增量”。这样一看表名,就知道它在哪一层、属于哪个主题、什么粒度、怎么刷新。
| 层级 | 前缀 | 示例 | 说明 |
|---|---|---|---|
| ODS | ods_ | ods_order | 贴源,保持原样 |
| DWD | dwd_ | dwd_trade_order_di | 明细,日增量 |
| DWS | dws_ | dws_trade_user_1d | 汇总,日粒度 |
| DM | dm_ | dm_sales_category_1d | 集市,日粒度 |
6.2 任务调度:依赖关系要清晰
分层架构的任务调度,核心是依赖关系。ODS 任务完成后,才能跑 DW 任务;DW 任务完成后,才能跑 DM 任务。我一般会用调度工具(比如 Airflow、DolphinScheduler)来管理这些依赖,每个任务配置上游依赖,上游失败则下游不跑。
这里有个小技巧:给关键任务加 SLA。比如 ODS 抽取必须在凌晨 2 点前完成,DW 加工必须在 4 点前完成,DM 刷新必须在 6 点前完成。一旦超时,立即告警,留出时间人工干预。
6.3 数据权限:分层管控
数据权限也要按层管控。ODS 层的数据最敏感,只对数据开发人员开放;DW 层对数据分析师开放;DM 层对业务方开放。这样既能保证数据安全,又能让不同角色的人各取所需。
我一般会用视图或行级权限来实现。比如业务方只能看 DM 层的表,而且只能看自己部门的数据。数据分析师可以看 DW 和 DM 层,但不能看 ODS 层。数据开发人员有全部权限,但操作要审计。
7. 分层之后,怎么衡量做得好不好
分层架构建完了,怎么判断它是不是成功?我一般会看三个指标:跑批时长、查询性能、维护成本。
跑批时长是硬指标。如果分层之后,跑批时间反而变长了,那说明分层设计有问题,可能是任务依赖太复杂,或者中间层数据量太大。我经历过一个项目,分层之后跑批时间从 2 小时变成 6 小时,排查发现是 DWS 层做了太多不必要的汇总,后来精简了汇总维度,时间降回 3 小时。
查询性能是业务方最关心的。DM 层的查询应该在秒级返回,如果业务方查个报表要等几分钟,那 DM 层的预计算就没做到位。我一般会要求 DM 层的核心报表查询响应时间在 3 秒以内,超过这个时间的,要么加索引,要么调整预计算逻辑。
维护成本是长期指标。如果每次加个指标都要改好几层,那说明分层太僵化。好的分层应该是可扩展的,加一个新指标,只需要在 DM 层加一张表,或者在 DWS 层加一个汇总维度,不需要动 ODS 和 DWD 层。
提示:我习惯每半年做一次分层架构的复盘,看看哪些层可以合并、哪些表可以下线、哪些流程可以自动化。分层不是一劳永逸的,它需要随着业务变化不断调整。
8. 我在实际项目中的几点体会
做了这么多年数据仓库,我最大的体会是:分层不是目的,而是手段。它的目的是让数据流更清晰、更可控、更高效。如果为了分层而分层,把架构搞得无比复杂,那就本末倒置了。
我见过一个团队,ODS、DWD、DWS、DM 四层齐全,每层都有几十张表,但业务方查个数据要等半小时,因为任务依赖太多,跑批时间太长。后来他们合并了 DWD 和 DWS,减少了中间层,跑批时间直接降了一半。所以,分层要适度,够用就好。
另一个体会是,文档和规范比技术更重要。分层架构的技术实现并不难,难的是让团队所有人都遵守同一套规范。表怎么命名、字段怎么定义、任务怎么调度、权限怎么管控,这些都需要写成文档,并且严格执行。我一般会把规范做成模板,新建表的时候直接套用,减少人为错误。
最后,数据质量是分层架构的生命线。再漂亮的分层,如果数据不准,那就是一堆垃圾。所以,不管分层怎么设计,数据质量监控一定要跟上。我一般会在每一层都加校验规则,比如 ODS 层校验行数和主键,DW 层校验关联率和空值率,DM 层校验指标波动。一旦发现异常,立即阻断下游,避免错误扩散。
这套分层方法论,我在几个项目里反复打磨过,不敢说放之四海而皆准,但至少能帮你少踩几个坑。如果你正在设计或重构数据仓库,不妨从 ODS、DW、DM 这三层开始,先把骨架搭起来,再慢慢填充细节。记住,分层是为了让数据更好用,不是为了让自己更忙。