简介:本资源是一份面向数据工程师、数仓开发人员及大数据初学者的系统性数据模型设计指南,聚焦数据仓库建设中的核心难点——如何科学分层与规范建模。文档深入解析ODS、CDM(含DWD明细层、DWS汇总层、DIM维表层)及ADS应用层的定位、边界与协同逻辑,详述数据域划分、总线矩阵构建、原子/派生指标定义、一致性维表与事实表设计等关键方法,并针对分层混乱、命名不统一、主键失当等高频问题提供可落地的解决方案与设计原则。资源为单文件Word文档(.doc),大小77KB,内容结构清晰、术语准确、实操性强,适合作为日常开发参考或团队建模规范宣贯材料。目前已有182人学习下载,涵盖从模型设计入门到分层治理进阶的完整知识链。
1. 数据仓库模型设计:不是画ER图,而是给企业建一座“数据立交桥”
你有没有见过这样的场景:
运营同学要查“过去30天新用户在首单后7天内的复购率”,提了需求,等了三天,被告知“这个指标底层没建模,得从ODS重新跑”;
BI工程师做一张销售看板,发现“省份”字段在用户表里叫province_name,在订单表里叫prov_code,在营销活动表里又变成region_id,硬着头皮写三段JOIN,结果某天源系统把prov_code字段类型从字符串改成整数,整张看板直接报错;
更常见的是——数据团队开了十几次对齐会,最后发现大家说的“活跃用户”,A认为是“当日登录+下单”,B理解为“近7日有任意行为”,C坚持必须包含“完成实名认证”,而D默默在ADS层已经建了三个同名不同义的dws_user_active_7d表……
这不是人的问题,是数据模型没设计好。
数据仓库模型设计,根本不是程序员坐在工位上画几张星型模型图、敲几行CREATE TABLE就完事的事。它更像是城市规划师设计一座“数据立交桥”:ODS是进城的原始国道(车流混杂、标牌不一),CDM是主干高架(分快慢车道、设统一出口编号、限速明确),ADS是直达商场的地下车库(每个入口只通一个品牌专柜,停车即用)。桥没设计好,车(数据)就堵在匝道(DWS层),导航(BI查询)总绕路,送货(指标交付)永远迟到。
这份《数据仓库|数据模型该如何设计?》文档,就是那份被无数团队翻烂、边角卷起、贴满便签纸的“立交桥施工白皮书”——它不教你怎么写SQL,而是告诉你:哪条路该修成高速,哪个出口必须预留扩容接口,为什么“复购率”不能直接从订单表算,而要先在DWD层把“用户-订单-商品”三者关系钉死成一张宽表。搞懂它,你才能从“取数民工”变成“数据基建工程师”。
2. 分层不是为了画PPT,而是给每类数据配一把专属“钥匙”
数据仓库分层,常被误解为“领导喜欢的架构图装饰”。但真实世界里,每一层的存在,都对应着一类不可妥协的数据治理诉求:谁负责维护、谁有权修改、谁来兜底质量、出问题时找谁。就像医院分诊台——不是为了好看,而是让发烧病人不用和骨折患者挤同一部电梯。
2.1 ODS层:数据的“海关入境处”,只登记,不改写
ODS(Operational Data Store)的本质,是数据的原始护照。它不做逻辑判断,不修正错误,甚至不翻译字段名。它的唯一使命:把业务系统(ERP、CRM、小程序后台)产生的原始数据,像海关扫描集装箱一样,原样、带时间戳、带来源标记地存进来。
-- 典型ODS建表逻辑(以MySQL为例) CREATE TABLE ods_order_2024 ( id BIGINT COMMENT '订单ID,来自源系统order_id', user_id STRING COMMENT '用户ID,来自源系统uid', status TINYINT COMMENT '订单状态,0=待支付,1=已支付,2=已发货...(照搬源系统枚举值)', create_time STRING COMMENT '字符串格式时间,如"2024-03-15 14:22:05",不转为DATE类型', __source_system STRING COMMENT '标记来源:crm_v2.3 / miniapp_v1.8 / erp_core', __load_time STRING COMMENT '本记录入库时间,用于后续核对' ) PARTITIONED BY (dt STRING) -- 按天分区,保留历史快照提示:ODS表名必须带
ods_前缀,且后缀体现来源系统(如ods_user_crm)。字段注释里强制写明“来自源系统XXX”,这是日后排查数据漂移的救命索引。
为什么不能在ODS层清洗?举个血泪经验:某公司曾在ODS把user_id为空的订单直接过滤掉,结果三个月后发现,这批“空ID订单”其实是小程序未登录用户的匿名订单,业务方正要用它们分析游客转化路径——数据已永久丢失。ODS的哲学是:“你给我什么,我存什么;你要怎么用,那是下一层的事”。
2.2 CDM层:数据的“中央厨房”,把生肉(ODS)加工成标准半成品(DWD/DWS/DIM)
CDM(Common Data Model)是整个数仓的“心脏区”,它由DWD、DWS、DIM三层组成,分工明确:
| 层级 | 全称 | 核心任务 | 类比生活 | 关键约束 |
|---|---|---|---|---|
| DWD | 明细层 | 基于业务过程建模,生成最细粒度事实表(如每笔订单、每次点击),冗余关键维度属性(如把province_id换成province_name) | 菜市场摊主把活鱼宰杀去鳞,按部位切块(鱼头、鱼身、鱼尾),每块贴上产地标签 | 只能引用ODS、DIM;禁止计算派生指标(如“复购率”) |
| DIM | 维表层 | 构建一致性维度表(用户、商品、地域、时间),确保全仓库“省份”只有一个定义 | 城市标准地图:所有楼盘、公交站、学校都按同一套行政区划编码标注 | 主键必须全局唯一(如dim_user.user_sk用MD5生成);属性变更必须拉链处理(start/end_time) |
| DWS | 汇总层 | 基于DWD+DIM,按公共粒度(如“日/用户/省份”)聚合指标,生成宽表(如dws_user_daily_summary) | 中央厨房预制菜:把鱼块、蔬菜、调料按固定比例配好,装进统一餐盒 | 只能引用DWD、DIM;指标命名必须含原子指标名(如pay_amt_sum而非total_money) |
注意:DWS不是“越多越好”。某电商曾建了200+张DWS表,结果发现80%的查询只用到其中7张。DWS存在的唯一理由:下游至少3个ADS应用需要相同粒度的同一组指标。否则,宁可在ADS层直接JOIN DWD。
2.3 ADS层:数据的“外卖专送站”,直抵业务最后一公里
ADS(Application Data Service)是离业务最近的一层,它不追求复用,只追求快、准、易懂。这里没有“应该建什么”,只有“业务现在要什么”。
-- 某运营活动看板专用ADS表(命名即含义) CREATE TABLE ads_promotion_conversion_dashboard ( dt DATE COMMENT '统计日期', promo_id STRING COMMENT '活动ID', promo_name STRING COMMENT '活动名称', new_user_cnt BIGINT COMMENT '新增用户数', pay_user_cnt BIGINT COMMENT '支付用户数', conversion_rate DECIMAL(5,4) COMMENT '转化率 = pay_user_cnt / new_user_cnt', avg_order_amt DECIMAL(10,2) COMMENT '客单价' ) COMMENT '大促活动转化漏斗看板,供BI拖拽使用';关键特征:
- 表名即业务语言:
ads_promotion_conversion_dashboard比dws_promo_summary更能让人一眼看懂; - 字段即业务口径:
conversion_rate的计算逻辑必须在注释里写死,避免BI工程师自行理解; - 拒绝通用化:这张表只为“大促看板”服务,绝不允许其他部门拿去当“用户增长分析”的基础表。
ADS层的黄金法则:宁可多建10张专表,也不建1张“万能表”。因为业务需求永远在变,而“万能表”的字段会越加越多,最终变成没人敢动的黑匣子。
3. 模型设计不是自由创作,而是遵守一套“数据宪法”
很多团队模型混乱的根源,在于把建模当成写作文——想怎么写就怎么写。但数据模型本质是一套契约:它约定好了“用户”是什么、“订单”怎么定义、“复购”如何计算。违反契约的代价,是全仓库的数据信任崩塌。
3.1 高内聚低耦合:把“一家人”关进同一间房,把“邻居”隔开
这是软件工程最古老的原则,在数据建模中却常被忽视。错误做法:把所有用户相关字段(注册信息、行为日志、支付记录)塞进一张dwd_user_all大宽表。结果呢?
- 注册字段更新频繁,导致整张表每天全量重刷;
- 行为日志字段极多(埋点JSON展开后上百列),但90%的ADS只用其中5个;
- 某天支付系统增加“分期期数”字段,为兼容老逻辑,不得不给全表加默认值,引发下游计算错误。
正确解法:按业务过程和访问模式拆分:
-- ✅ 高内聚:用户注册事实表(DWD) CREATE TABLE dwd_user_register_fct ( user_sk STRING COMMENT '用户代理键', register_time DATETIME, channel STRING COMMENT '注册渠道:微信/APP/网页', device_type STRING COMMENT '设备:iOS/Android/H5' ); -- ✅ 高内聚:用户支付事实表(DWD) CREATE TABLE dwd_user_pay_fct ( user_sk STRING, order_id STRING, pay_time DATETIME, pay_amt DECIMAL(10,2), installments TINYINT COMMENT '分期期数,新字段不影响注册表' ); -- ✅ 低耦合:两表通过user_sk关联,但物理存储分离 -- 下游ADS若只需注册数据,只读dwd_user_register_fct;需支付数据则JOIN两者核心判断标准:如果两个字段总是被一起查询、一起更新、业务含义强绑定,才放同一张表。否则,宁可多一次JOIN,也要保持模型轻量。
3.2 核心模型与扩展模型分离:给“主干道”和“辅路”划清车道线
所有模型都该有“主干”和“枝叶”之分。主干模型(Core Model)承载80%的通用需求,必须稳定、简洁、高性能;扩展模型(Extension Model)满足长尾需求,允许试错、可随时下线。
典型反例:某公司dwd_order_fct表里,既有order_id、user_id、pay_amt等核心字段,又有delivery_address_json(配送地址JSON)、coupon_detail_json(优惠券明细JSON)等半结构化字段。结果:
- 核心字段查询因JSON解析拖慢3倍;
- 某次优惠券规则变更,JSON结构升级,导致所有依赖该表的ADS全部报错;
- 新增“订单履约时效”指标时,开发被迫在核心表加
first_pickup_time字段,破坏了“订单事实”的单一职责。
正确姿势:
-- ✅ 核心事实表(只含原子字段) CREATE TABLE dwd_order_fct_core ( order_id STRING, user_id STRING, shop_id STRING, pay_amt DECIMAL(10,2), create_time DATETIME, pay_time DATETIME ); -- ✅ 扩展表(独立存储,按需JOIN) CREATE TABLE dwd_order_fct_ext ( order_id STRING, delivery_address_json STRING, coupon_detail_json STRING, first_pickup_time DATETIME );血泪经验:扩展字段上线前,必须回答三个问题:① 是否影响核心指标计算?② 是否被超过2个ADS高频使用?③ 是否有明确的下线计划?任一答案为“否”,就放进扩展表。
3.3 公共逻辑下沉:别让每个ADS都重复造轮子
最典型的“重复造轮子”场景:10个ADS都需要“用户最近一次下单时间”。如果每个ADS都写一遍ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY pay_time DESC),后果是:
- 计算资源浪费300%(10次相同窗口函数);
- 某天业务要求“最近一次下单”改为“最近一次有效下单(排除退款订单)”,要改10处代码;
- 其中3个ADS忘了改,导致数据不一致。
解决方案:把公共逻辑封装进DWS层。
-- ✅ 在DWS层统一计算(dws_user_last_order_d) CREATE TABLE dws_user_last_order_d AS SELECT user_id, MAX(pay_time) AS last_pay_time, COUNT(*) AS total_order_cnt FROM dwd_order_fct_core WHERE pay_status = 'paid' -- 此处统一定义“有效订单” GROUP BY user_id;原则:凡是涉及业务规则、状态判断、复杂计算的逻辑,必须下沉到CDM层。ADS层只做简单筛选(WHERE)、轻量聚合(SUM/COUNT)、业务定制化拼接(如把用户昵称+头像URL拼成
user_profile字段)。
4. 常见误区:你以为的“规范”,可能是团队最大的技术债
初学者最容易掉进的坑,往往披着“高效”“灵活”“快速”的外衣。这些误区不会立刻暴雷,但会在6个月后让整个数仓变成不敢动的“瓷器店”。
4.1 误区一:ADS可以直接读ODS,省事又快
为什么混:刚上线时,业务催得紧,开发觉得“ODS里就有订单表,直接查它不就完了?”
怎么区分:
- ✅可以:临时救火(如老板凌晨要一份数据,明天就上线);
- ❌绝对禁止:作为长期方案。ODS字段名、类型、枚举值随源系统变更,ADS直接依赖等于把命交给业务系统。
真实案例:某金融公司ADS层有张ads_risk_monitor表,直接JOINods_loan_apply。某天信贷系统升级,把apply_status从0/1/2改为APPLYING/APPROVED/REJECTED,导致风控看板所有状态显示为NULL,实时监控中断4小时。
正确姿势:所有ADS必须通过DWD或DWS层获取数据。哪怕DWD还没建好,也应先建一张最小化的dwd_loan_apply_fct(只映射关键字段),再让ADS依赖它。
4.2 误区二:DIM维表用源系统主键(如user_id)当代理键
为什么混:源系统主键天然唯一,何必多此一举生成user_sk?
怎么区分:
- ✅源系统主键:可用于查询关联(JOIN条件),但绝不能作为维度主键;
- ❌DIM主键必须是代理键(Surrogate Key):如
MD5(CONCAT(user_id, source_system))。
原因有三:
- 解决多源冲突:用户A在CRM里
user_id=1001,在小程序里user_id=1001,代理键可区分; - 支持缓慢变化:用户所在省份从“广东”变“深圳”,代理键不变,用拉链表记录变更;
- 屏蔽源系统变更:源系统
user_id类型从INT改为BIGINT,代理键完全不受影响。
4.3 误区三:DWS汇总表越“宽”越好,把所有可能用到的字段都冗余进去
为什么混:宽表查询快,BI拖拽方便,何乐不为?
怎么区分:
- ✅合理冗余:维度属性(如
province_name,category_name)且极少变更; - ❌禁止冗余:事实字段(如
pay_amt,order_cnt)、半结构化字段(JSON)、高频更新字段(如user_balance)。
性能真相:某电商DWS宽表冗余了200+字段,单表大小超500GB。一次简单SELECT province_name, SUM(pay_amt)查询,因需扫描全部列,耗时47秒;而拆分为dws_province_daily(仅含地域+指标)后,同样查询仅0.8秒。
口诀:DWS宽表只冗余“静态维度名”,不冗余“动态事实值”。
4.4 误区四:分层边界模糊,“DWS和ADS到底差在哪?”
为什么混:文档写了定义,但实际开发中,有人把ADS表命名为dws_xxx,有人把DWS表当ADS用。
怎么区分(用一张表诊断):
| 判断项 | DWS层 | ADS层 |
|---|---|---|
| 建设目标 | 支持≥3个ADS的公共指标 | 服务1个具体业务场景 |
| 数据粒度 | 全局统一(如“日+用户+省份”) | 场景定制(如“活动ID+渠道+小时”) |
| 字段命名 | 含原子指标名(pay_amt_sum) | 含业务术语(gmv_today) |
| 更新频率 | 固定周期(T+1) | 按需(实时/分钟级/活动期间) |
| 下游依赖 | 多个ADS表 | 仅BI看板/某个API |
自查工具:如果一张表的注释里写着“供XX部门XX看板使用”,它就是ADS;如果写着“支撑用户域公共指标”,它就是DWS。
5. 什么时候该建DWS?什么时候该砍掉DWS?——用一张决策表终结纠结
建DWS不是技术选择,而是成本收益权衡。很多团队陷入“为建而建”的陷阱,结果DWS成了吃资源不产出的“数据盆景”。下面这张表,是我带过的5个团队踩坑后总结的DWS生死线。
5.1 DWS建设决策表:5个问题,答错1个就暂停
| 问题 | 是 | 否 | 决策 |
|---|---|---|---|
| Q1:这个指标是否被≥3个ADS应用高频使用? (高频=每周查询≥5次,或支撑核心报表) | ✓ | ✗ | 否→跳过DWS,让ADS直接JOIN DWD |
| Q2:这些ADS是否对指标口径、计算逻辑、更新时效有完全一致的要求? (如都要求“T+1”,都定义“复购=7日内二次支付”) | ✓ | ✗ | 否→必须拆分成多个DWS(如dws_user_rebuy_7d_t1和dws_user_rebuy_7d_rt) |
| Q3:DWD层是否已稳定提供所需原子数据? (DWD表无重大BUG,SLA达标) | ✓ | ✗ | 否→先修复DWD,DWS是“果”,DWD是“根” |
| Q4:当前DWS层同类粒度的表是否已超负荷? (如“日+用户”粒度DWS表已达15张,单日计算耗时超2小时) | ✗ | ✓ | 是→优先优化现有DWS(合并、分区裁剪),而非新建 |
| Q5:业务方是否愿意为DWS投入维护成本? (明确指定负责人,承诺参与口径评审,接受DWS变更通知) | ✓ | ✗ | 否→暂缓,DWS不是“技术自嗨”,没有业务Owner的DWS注定废弃 |
真实案例:某直播平台想建
dws_anchor_live_summary(主播直播汇总表)。按表自查:
- Q1:仅运营看板和主播激励系统2个应用使用 →否;
- Q2:运营要看“开播时长”,激励系统要“有效开播时长(观众≥10人)” →否;
- Q3:DWD层
dwd_live_session_fct存在数据延迟问题 →否。
结论:不建DWS。改为在ADS层各自实现,等业务需求收敛、DWD稳定后再评估。
5.2 DWS“砍伐指南”:这3种DWS请立即下线
不是所有DWS都值得保留。以下三类,建议每季度巡检,发现即下线:
- 幽灵DWS:建表后6个月内,无任何ADS或BI查询记录(可通过数仓元数据系统查
last_access_time); - 孤儿DWS:依赖的DWD表已下线,或上游源系统已停运;
- 僵尸DWS:字段中超过50%为NULL值,或90%的查询只用其中3个字段(说明设计过度冗余)。
操作步骤:
① 在调度系统中将该DWS任务置为“暂停”;
② 发邮件通知所有潜在使用者(抄送数据负责人);
③ 设置30天观察期,期间无反馈则执行DROP TABLE;
④ 将建表语句、血缘关系存入归档库,备查。
从那以后我每次设计DWS前,都会打开这张表,逐条打钩。不是为了“显得规范”,而是因为少建一张无用的DWS,就能省下2TB存储、每天1.7小时计算资源、以及未来3个工程师排查问题的200小时。数据基建不是堆砌,而是克制;不是画更大的饼,而是切更准的刀。希望帮到你。
本文还有配套的精品资源,点击获取