事实表和维度表到底怎么设计?从星型模型到 Kimball 维度建模
上一篇我们分析了企业为什么要把数据仓库划分为 ODS、DWD、DWS、ADS。
但是理解数仓分层以后,还会遇到一个更实际的问题:
DWD 层里的表到底应该怎么设计?
很多人刚接触数据仓库时,会直接把 MySQL 里的业务表同步到 Hive、Doris 或 StarRocks,然后继续按照业务数据库的方式写 SQL。
结果是:
- 查询需要关联大量业务表;
- 同一指标被重复开发;
- 数据粒度混乱;
- Join 后金额和订单数被重复计算;
- 历史状态无法准确还原;
- 表越来越多,却不知道哪些是真正可信的数据。
这些问题的本质不是 SQL 写得不够好,而是数据模型没有设计好。
本文将从一个电商业务案例出发,逐步解释:
- 为什么业务表不适合直接分析;
- 什么是 Kimball 维度建模;
- 事实表应该如何设计;
- 维度表应该如何设计;
- 为什么声明粒度是建模中最重要的一步。
一、为什么业务表不能直接拿来分析
假设一家电商公司有下面几个业务系统:
用户系统
商品系统
订单系统
支付系统
库存系统
物流系统
优惠券系统
每个系统都有独立的数据库和数据表。
订单系统可能包含:
orders
order_items
order_status_log
商品系统可能包含:
products
product_categories
brands
suppliers
用户系统可能包含:
users
user_addresses
user_levels
支付系统可能包含:
payments
refunds
payment_channels
这些表最初都是为了支撑业务系统运行,而不是为了数据分析。
1.1 业务数据库的目标是完成交易
用户下单时,业务系统最关心的是:
- 订单能不能成功创建;
- 库存能不能正确扣减;
- 支付是否成功;
- 事务是否一致;
- 单笔请求响应是否足够快。
例如,根据订单号查询一笔订单:
SELECT
order_id,
user_id,
order_status,
pay_amount
FROM orders
WHERE order_id = 10001;
这种查询通常只读取一条或者少量数据。
业务数据库非常适合这种场景。
但数据分析关注的问题完全不同。
例如,运营人员可能提出:
统计最近30天,福建地区黄金会员购买手机类商品的订单量、支付金额和退款率。
这时可能需要关联:
订单表
订单明细表
用户表
用户等级表
用户地址表
商品表
商品分类表
支付表
退款表
地区表
查询可能变成:
SELECT
COUNT(DISTINCT o.order_id) AS order_count,
SUM(oi.pay_amount) AS pay_amount,
SUM(r.refund_amount) / SUM(oi.pay_amount) AS refund_rate
FROM orders o
JOIN order_items oi
ON o.order_id = oi.order_id
JOIN users u
ON o.user_id = u.user_id
JOIN user_levels ul
ON u.level_id = ul.level_id
JOIN user_addresses ua
ON u.default_address_id = ua.address_id
JOIN regions rg
ON ua.region_id = rg.region_id
JOIN products p
ON oi.product_id = p.product_id
JOIN product_categories pc
ON p.category_id = pc.category_id
LEFT JOIN refunds r
ON oi.order_item_id = r.order_item_id
WHERE rg.province_name = '福建'
AND ul.level_name = '黄金会员'
AND pc.category_name = '手机'
AND o.create_time >= CURRENT_DATE - INTERVAL 30 DAY;
这条 SQL 即使能够执行,也会面临多个问题。
1.2 业务表结构过于分散
业务系统通常遵循三范式设计。
例如商品相关信息可能拆成:
product
↓
category
↓
brand
↓
supplier
用户地区可能拆成:
user
↓
address
↓
city
↓
province
↓
country
这种结构可以减少数据冗余,也方便业务系统单独维护。
但是在分析场景中,每次查询都要执行多层 Join。
随着数据量增大,SQL 会越来越复杂,查询成本也会越来越高。
1.3 相同业务含义可能分散在不同表中
例如“订单支付金额”,可能存在于:
- 订单主表;
- 订单明细表;
- 支付流水表;
- 对账表;
- 退款表。
不同系统中的金额字段可能代表不同含义:
order_amount:订单原始金额
discount_amount:优惠金额
pay_amount:实际支付金额
refund_amount:退款金额
settlement_amount:结算金额
如果数据开发人员不了解业务,很容易选错字段。
1.4 Join 可能导致数据重复
假设一笔订单中包含3个商品,同时发生2次支付尝试。
订单主表中只有一条记录:
订单明细表中有3条记录:
支付表中有2条记录:
如果直接把三张表 Join:
1条订单 × 3条商品明细 × 2条支付记录 = 6条结果
此时再执行:
SUM(pay_amount)
原本1000元的订单,可能被统计成6000元。
这不是 SQL 语法错误,而是数据粒度没有处理正确。
1.5 业务表只反映当前状态
假设用户在下单时是普通会员,一个月后升级为黄金会员。
业务用户表中可能只保留当前状态:
如果直接用当前用户表关联历史订单,系统会认为这名用户过去所有订单都是黄金会员订单。
但实际情况是:
2026-01-01:普通会员
2026-03-01:黄金会员
如果企业要分析不同会员等级下的消费行为,就必须保存历史状态。
业务系统通常不负责这个问题,数据仓库需要通过维度建模解决。
1.6 数据分析需要重新组织数据
因此,数据仓库不能简单复制业务数据库结构。
数据仓库需要围绕分析需求,重新组织数据。
业务数据库通常按照系统组织:
订单系统
用户系统
商品系统
支付系统
数据仓库更适合按照业务过程组织:
下单
支付
退款
发货
浏览
登录
库存变化
这就是数据仓库建模要解决的核心问题:
把面向事务的数据结构,转换成面向分析的数据结构。
二、什么是 Kimball 维度建模
数据仓库建模有多种方法,其中最常见的是 Kimball 维度建模。
Kimball 维度建模的核心思想可以概括为:
围绕业务过程设计事实表,用维度描述业务事实发生时的背景。
它不是按照源系统有多少张表,就在数据仓库里复制多少张表。
它首先关心的是:
- 企业发生了哪些业务过程;
- 每个业务过程产生了哪些可分析事件;
- 每条事实记录代表什么;
- 可以从哪些角度分析这些事实;
- 事实中有哪些可以累计或计算的数值。
2.1 什么是业务过程
业务过程是企业中持续发生、可以被记录和分析的行为。
例如电商场景中的业务过程包括:
用户注册
商品浏览
加入购物车
提交订单
完成支付
申请退款
完成发货
签收商品
评价商品
其中,“订单系统”是一个业务系统,而“用户下单”才是一个业务过程。
“支付系统”是一个业务系统,而“支付成功”才是一个可以建模的业务过程。
Kimball 建模强调围绕业务过程组织数据,而不是围绕应用系统组织数据。
2.2 事实和维度
维度建模把数据主要分成两类:
事实 Fact
维度 Dimension
事实描述发生了什么。
例如:
- 用户下了一笔订单;
- 用户支付了4500元;
- 商品被浏览了一次;
- 系统完成了一次退款;
- 仓库发生了一次库存扣减。
维度描述事实发生时的背景。
例如:
- 是哪个用户;
- 购买了什么商品;
- 在什么时间发生;
- 来自哪个地区;
- 通过哪个渠道;
- 参加了什么活动;
- 属于哪个店铺。
例如,一笔订单事实可以表示为:
用户20001
在2026年8月2日
通过App渠道
在福建地区
购买商品30001
支付金额4500元
其中:
- 支付金额是事实中的度量值;
- 用户、时间、渠道、地区、商品是分析维度。
2.3 Kimball 建模四步法
Kimball 维度建模通常可以按照四个步骤完成。
第一步:选择业务过程
先明确要分析什么业务过程。
例如:
订单创建
订单支付
订单退款
不要一开始就考虑字段,也不要先照着业务表建表。
第二步:声明粒度
明确事实表中一行数据代表什么。
例如:
一行代表一个订单中的一个商品
或者:
一行代表一次支付成功事件
粒度不同,事实表的字段和主键都会不同。
第三步:确定维度
确定从哪些角度分析业务事实。
例如订单明细事实可以关联:
- 用户维度;
- 商品维度;
- 店铺维度;
- 时间维度;
- 地区维度;
- 渠道维度;
- 活动维度。
第四步:确定事实
确定需要保存哪些可计算数据。
例如:
- 商品数量;
- 商品原价;
- 优惠金额;
- 实际支付金额;
- 退款金额;
- 运费;
- 平台补贴金额。
完整过程可以概括为:
选择业务过程
↓
声明粒度
↓
确定维度
↓
确定事实
顺序不能颠倒。
很多建模问题都来自一开始没有确定粒度,就直接往表里添加字段。
2.4 Kimball 模型为什么适合分析
Kimball 模型通常采用事实表加维度表的结构。
例如:
时间维度
|
用户维度 —— 订单事实表 —— 商品维度
|
地区维度
|
渠道维度
查询时,分析人员可以围绕事实表,从不同维度进行切分。
例如:
按时间统计销售额
按地区统计销售额
按用户等级统计销售额
按商品品类统计销售额
按渠道统计销售额
这比每次从多个业务系统重新理解字段和编写复杂 Join 更稳定。
2.5 维度建模不等于做一张大宽表
很多人把维度建模理解成:
把所有业务表 Join 成一张特别宽的表。
这并不准确。
宽表是一种物理实现方式,维度建模是一种数据组织方法。
维度建模首先要求:
- 业务过程清晰;
- 粒度清晰;
- 事实和维度职责清晰;
- 指标口径清晰;
- 历史变化处理清晰。
至于最终是否把部分维度字段冗余到事实表中,要根据查询性能、存储成本和实时处理方式决定。
三、事实表到底是什么
事实表英文为 Fact Table。
它用于记录业务过程中已经发生的事件,以及这些事件产生的可度量结果。
可以用一句话概括:
事实表记录业务事件。
例如电商平台中,可以设计:
订单事实表
支付事实表
退款事实表
商品浏览事实表
用户登录事实表
库存变更事实表
物流签收事实表
3.1 如何判断一个业务对象是不是事实
可以从下面几个问题判断:
- 它是否代表一个已经发生的业务行为?
- 数据是否会随着业务持续新增?
- 是否有明确的发生时间?
- 是否可以计算次数、金额、数量或时长?
- 是否需要从多个角度进行分析?
例如“支付成功”:
- 是一个业务事件;
- 每天会不断产生;
- 有支付时间;
- 有支付金额;
- 可以按用户、渠道、地区、商品分析。
因此,支付适合设计为事实表。
而“商品品牌”本身不是一个持续发生的业务事件,它更适合作为维度。
3.2 事实表通常包含什么字段
事实表通常包含三类字段。
业务标识
用于识别业务事件。
例如:
order_id
order_detail_id
payment_id
refund_id
维度键
用于关联不同分析维度。
例如:
user_id
product_id
shop_id
province_id
channel_id
activity_id
度量值
表示业务事件产生的数值结果。
例如:
quantity
original_amount
discount_amount
pay_amount
refund_amount
shipping_fee
一个简化的订单明细事实表可能是:
CREATE TABLE fact_order_detail (
order_detail_id BIGINT,
order_id BIGINT,
user_id BIGINT,
product_id BIGINT,
shop_id BIGINT,
province_id BIGINT,
channel_id BIGINT,
product_quantity INT,
original_amount DECIMAL(18, 2),
discount_amount DECIMAL(18, 2),
pay_amount DECIMAL(18, 2),
order_time DATETIME,
dt DATE
);
3.3 度量值是什么
度量值是可以参与统计或计算的数据。
例如:
- 订单金额;
- 商品数量;
- 支付金额;
- 退款金额;
- 浏览时长;
- 配送时长;
- 库存变化数量。
度量值通常可以进行:
SUM
COUNT
AVG
MAX
MIN
但并不是所有数值字段都是度量值。
例如:
user_id = 10001
product_id = 30001
province_id = 35
它们虽然是数字,但只是标识,不适合求和。
3.4 事实表中的事实类型
度量值通常可以分为三类。
可加事实
可以在所有维度上累加。
例如:
- 支付金额;
- 商品数量;
- 退款金额;
- 优惠金额。
例如:
SUM(pay_amount)
可以按时间、地区、商品等维度汇总。
半可加事实
只能在部分维度上累加。
例如账户余额、库存余额。
某商品每天的库存分别是:
不能计算:
100 + 80 + 70 = 250
因为库存是某个时间点的状态。
它可以在商品、仓库等维度上累加,但不能简单跨时间累加。
不可加事实
不能直接累加。
例如:
- 转化率;
- 退款率;
- 客单价;
- 平均配送时间。
例如每天的退款率分别是10%和20%,不能直接相加成30%。
通常应该在事实表中保存原始分子和分母:
refund_amount
pay_amount
查询时再计算:
SUM(refund_amount) / SUM(pay_amount)
而不是直接汇总每天计算好的退款率。
3.5 常见事实表类型
事实表不只一种。
事务事实表
一行代表一次业务事件。
例如:
一笔订单明细
一次支付
一次退款
一次商品浏览
事务事实表通常数据量最大,也最常见。
周期快照事实表
按照固定时间间隔记录业务状态。
例如:
每天每个商品的库存余额
每天每个用户的账户余额
每月每个客户的信用额度
例如:
累积快照事实表
一行记录一个业务流程从开始到结束的关键节点。
例如订单履约过程:
创建时间
支付时间
发货时间
签收时间
完成时间
表结构可能是:
CREATE TABLE fact_order_lifecycle (
order_id BIGINT,
user_id BIGINT,
create_time DATETIME,
pay_time DATETIME,
ship_time DATETIME,
receive_time DATETIME,
finish_time DATETIME
);
随着订单状态推进,同一行会被持续更新。
这种模型适合分析:
- 从下单到支付耗时;
- 从支付到发货耗时;
- 从发货到签收耗时;
- 整个订单履约周期。
3.6 事实表中的退化维度
有些字段具有维度性质,但没有必要单独建维度表。
例如:
订单号
支付流水号
物流单号
优惠券使用编号
这些字段通常直接保存在事实表中。
它们被称为退化维度。
例如订单号可以用于:
- 查询某一笔订单;
- 统计去重订单数;
- 关联订单明细。
但是单独建立一张只有订单号的维度表,没有实际价值。
3.7 事实表不是业务表的复制
业务订单表可能同时包含:
订单当前状态
用户收货地址
支付信息
物流信息
优惠券信息
订单金额
售后状态
数据仓库不应该照搬这张表。
需要先区分业务过程:
下单事实
支付事实
退款事实
物流事实
每个业务过程分别建模,粒度和度量更加清晰。
四、维度表到底是什么
维度表英文为 Dimension Table。
维度用于描述事实发生时的业务背景。
可以用一句话概括:
事实表回答“发生了什么”,维度表回答“在什么背景下发生”。
例如一笔支付事实是:
用户20001支付了4500元
如果只保存这一句话,分析价值有限。
加入维度后,可以描述为:
用户20001
在2026年8月2日
通过App渠道
在福建地区
购买手机类商品
支付了4500元
这里:
- 用户;
- 日期;
- 渠道;
- 地区;
- 商品;
都属于维度。
4.1 常见维度有哪些
电商场景中常见维度包括:
用户维度
商品维度
店铺维度
地区维度
时间维度
渠道维度
活动维度
品牌维度
供应商维度
不同业务领域还会有不同维度。
例如金融领域:
客户维度
账户维度
机构维度
产品维度
风险等级维度
游戏领域:
玩家维度
角色维度
服务器维度
游戏版本维度
活动维度
设备维度
4.2 维度表通常包含描述性字段
用户维度可能包含:
user_id
user_name
gender
age_group
member_level
register_channel
register_date
province
city
商品维度可能包含:
product_id
product_name
category_name
brand_name
supplier_name
price_range
product_status
维度表中的字段通常用于:
- 查询过滤;
- 分组统计;
- 报表展示;
- 数据分类;
- 标签分析。
例如:
SELECT
product_category,
SUM(pay_amount)
FROM fact_order_detail f
JOIN dim_product p
ON f.product_id = p.product_id
GROUP BY product_category;
4.3 维度表和事实表有什么区别
例如:
fact_payment
每天可能新增几百万甚至几亿条。
而:
dim_region
可能只有几千条记录,并且变化很少。
但“维度表数据量一定小”不是绝对规则。
例如用户维度可能有数亿条数据,只是相对于点击日志、订单明细等事实数据,增长频率通常较低。
4.4 维度表为什么经常设计得比较宽
业务数据库通常会把商品拆成:
商品表
商品分类表
品牌表
供应商表
在维度建模中,可能将这些描述属性合并到商品维度:
CREATE TABLE dim_product (
product_id BIGINT,
product_name STRING,
category_id BIGINT,
category_name STRING,
brand_id BIGINT,
brand_name STRING,
supplier_id BIGINT,
supplier_name STRING,
price_range STRING,
product_status STRING
);
这样查询时只需要关联一次商品维度。
优点包括:
- 减少 Join;
- 查询逻辑更简单;
- 对分析人员更友好;
- 更适合星型模型;
- 提升 OLAP 查询效率。
代价是存在一定数据冗余。
但数据仓库更看重查询便利和口径统一,不像业务数据库那样极度追求减少冗余。
4.5 自然键和代理键
维度表中经常涉及两种键。
自然键
来自业务系统的真实主键。
例如:
user_id = 10001
product_id = 30001
代理键
由数据仓库自己生成的主键。
例如:
user_sk = 900001
product_sk = 800001
代理键通常没有业务含义,只用于维度版本管理和事实表关联。
例如用户等级发生变化时,可以生成两个维度版本:
虽然业务用户 ID 都是10001,但两个历史版本对应不同的代理键。
历史订单关联900001,新订单关联900002。
这样就能准确保留用户状态变化。
4.6 时间维度为什么也要单独建表
很多人会问:
事实表里已经有日期字段,为什么还需要时间维度?
时间维度不仅保存日期,还可以保存丰富的时间属性。
例如:
date_key
full_date
year
quarter
month
week_of_year
day_of_month
day_of_week
is_weekend
is_holiday
holiday_name
fiscal_year
通过时间维度,可以方便分析:
- 工作日和周末差异;
- 节假日销售趋势;
- 财务季度表现;
- 月度和周度汇总。
如果每次都在 SQL 中计算这些字段,会产生大量重复逻辑。
4.7 一致性维度
Kimball 建模中一个重要概念是一致性维度,也叫共享维度。
例如订单事实表、支付事实表和退款事实表,都应该使用统一的:
用户维度
商品维度
地区维度
时间维度
而不是每个业务主题自己创建一套不同的用户定义。
例如:
订单主题中的用户等级
支付主题中的用户等级
退款主题中的用户等级
如果定义不同,就无法在不同事实表之间进行一致分析。
一致性维度可以让多个业务过程共享统一分析口径。
五、建模最重要的一步:声明粒度
在事实表设计中,最重要的不是字段数量,也不是表名,而是粒度。
粒度英文为 Grain。
它表示:
事实表中一行数据究竟代表什么。
在设计事实表前,必须先用一句完整的话声明粒度。
例如:
一行代表一个订单
或者:
一行代表一个订单中的一个商品
或者:
一行代表一次支付行为
或者:
一行代表一个订单的一次状态变化
这几种表都与订单有关,但它们不是同一张事实表。
5.1 订单可以有多种粒度
假设订单10001包含两个商品:
订单粒度
一行代表一笔订单:
适合分析:
- 订单数;
- 客单价;
- 每笔订单商品数量;
- 订单状态。
订单商品粒度
一行代表订单中的一个商品:
适合分析:
- 商品销量;
- 品类销售额;
- 品牌贡献;
- 单品退款率。
支付粒度
如果订单发生两次支付尝试:
一行代表一次支付行为。
适合分析:
- 支付成功率;
- 支付渠道表现;
- 支付失败原因;
- 重试次数。
状态变化粒度
一行代表一次状态变化。
适合分析:
- 状态流转;
- 订单停留时长;
- 履约效率;
- 异常订单。
5.2 粒度混乱会导致什么问题
假设一张表中既有订单字段,又有商品明细字段:
如果执行:
SELECT SUM(order_amount)
FROM fact_order_detail;
结果是:
2000
但真实订单金额只有1000。
因为订单金额属于订单粒度,却被重复写入订单商品粒度的每一行。
这类问题在真实项目中非常常见。
解决方式包括:
- 不在商品粒度中存订单级金额;
- 将订单金额合理拆分到商品;
- 查询时按订单去重;
- 单独建立订单粒度事实表。
最重要的是,字段必须与表的粒度一致。
5.3 先声明粒度,再选择字段
假设要设计订单明细事实表。
首先声明:
一行代表一个订单中的一个商品明细。
然后再确定字段。
适合保存:
order_detail_id
order_id
product_id
user_id
quantity
product_original_amount
product_discount_amount
product_pay_amount
不适合直接保存:
整单支付金额
整单运费
整单优惠金额
订单包含商品种类数
因为这些字段属于订单级别。
除非已经有明确、可解释的分摊规则,否则不应该直接复制到每个明细行中。
5.4 粒度决定主键
如果粒度是一行一个订单,主键可能是:
order_id
如果粒度是一行一个订单商品,主键可能是:
order_detail_id
或者:
order_id + product_id + sku_id
如果粒度是一行一次支付,主键应该是:
payment_id
如果粒度是一行一次状态变化,主键可能是:
order_id + status + change_time
因此,粒度不明确时,主键也无法正确设计。
5.5 粒度决定维度
订单粒度可能关联:
用户
地区
渠道
下单时间
订单商品粒度还可以关联:
商品
品类
品牌
供应商
支付粒度还可以关联:
支付渠道
支付机构
支付方式
粒度决定了哪些维度能够合理地出现在事实表中。
5.6 粒度决定度量值
订单商品粒度适合保存:
商品数量
商品金额
商品优惠金额
商品支付金额
支付粒度适合保存:
支付请求金额
支付成功金额
支付手续费
库存快照粒度适合保存:
期末库存数量
可售库存数量
锁定库存数量
如果一个度量无法与当前粒度一一对应,就需要重新考虑设计。
5.7 原子粒度优先原则
在大多数情况下,事实表应该尽量保存最低的、可用的原子粒度。
例如订单分析中:
一个订单中的一个商品
通常比“一天一个品类”更接近原始业务事实。
原子粒度的优势是:
- 可以支持更多分析场景;
- 可以向上汇总;
- 可以重新计算指标;
- 数据可追溯性更强。
例如订单商品明细可以汇总成:
订单粒度
商品粒度
品类粒度
用户粒度
地区粒度
日期粒度
但如果一开始只保存每天每个品类的销售额,就无法反向还原具体订单和用户。
不过,原子粒度不代表所有事件都放进一张表。
不同业务过程仍然需要独立事实表。
5.8 声明粒度的标准写法
建模文档中不要只写:
订单明细表
应该明确写:
本表粒度为一行代表一个订单中的一个商品明细。
支付事实表应该写:
本表粒度为一行代表一次支付请求。
用户日活跃快照表应该写:
本表粒度为一天、一个用户一条记录。
商品库存快照表应该写:
本表粒度为一天、一个仓库、一个商品一条记录。
只要这句话无法准确说清,就说明模型还没有设计完成。
5.9 第五章小结
粒度决定:
- 一行数据代表什么;
- 表的主键是什么;
- 可以关联哪些维度;
- 可以保存哪些度量值;
- 数据如何聚合;
- 是否会产生重复计算。
因此,维度建模中最重要的原则之一是:
先声明粒度,再设计事实表。
Kimball 四步法之所以把“声明粒度”放在确定维度和事实之前,就是因为后面的所有设计都依赖粒度。
接下来需要继续解决的问题是:
- 事实表和维度表如何组合;
- 什么是星型模型;
- 什么是雪花模型;
- 为什么企业分析场景通常更偏向星型模型;
- 用户等级、商品分类等维度发生变化时,历史数据应该如何保存。
六、星型模型与雪花模型
事实表和维度表确定以后,还需要考虑它们之间如何组织。
在维度建模中,最常见的两种结构是:
- 星型模型;
- 雪花模型。
两者的核心区别,不是有没有事实表和维度表,而是维度是否继续拆分。
6.1 什么是星型模型
星型模型英文为 Star Schema。
它的结构通常是:
- 事实表位于中心;
- 多张维度表分布在事实表四周;
- 事实表直接关联各个维度表;
- 维度表之间通常不再继续关联。
例如电商订单分析模型:
dim_date
|
|
dim_user —— fact_order_detail —— dim_product
|
|
dim_region
|
|
dim_channel
中心是订单明细事实表:
fact_order_detail
周围是:
dim_user
dim_product
dim_date
dim_region
dim_channel
从结构上看,像一颗星,所以被称为星型模型。
6.2 星型模型中的事实表
订单明细事实表可能包含:
CREATE TABLE fact_order_detail (
order_detail_id BIGINT,
order_id BIGINT,
user_sk BIGINT,
product_sk BIGINT,
date_sk INT,
region_sk BIGINT,
channel_sk BIGINT,
product_quantity INT,
original_amount DECIMAL(18, 2),
discount_amount DECIMAL(18, 2),
pay_amount DECIMAL(18, 2),
order_time DATETIME
);
其中:
user_sk
product_sk
date_sk
region_sk
channel_sk
用于关联维度表。
而:
product_quantity
original_amount
discount_amount
pay_amount
属于事实表中的度量值。
6.3 星型模型中的维度表
商品维度可能是:
CREATE TABLE dim_product (
product_sk BIGINT,
product_id BIGINT,
product_name STRING,
category_id BIGINT,
category_name STRING,
brand_id BIGINT,
brand_name STRING,
supplier_id BIGINT,
supplier_name STRING,
price_range STRING,
product_status STRING
);
这里没有继续拆成:
商品表
分类表
品牌表
供应商表
而是把常用的商品描述属性放在同一张维度表中。
这会产生一定冗余,但查询时更简单。
例如,统计不同商品品类的销售额:
SELECT
p.category_name,
SUM(f.pay_amount) AS pay_amount
FROM fact_order_detail f
JOIN dim_product p
ON f.product_sk = p.product_sk
GROUP BY p.category_name;
只需要关联一张商品维度。
6.4 星型模型为什么适合分析
星型模型的第一个优点是查询简单。
分析人员通常只需要:
事实表
+
一个或多个维度表
不需要理解业务数据库中复杂的表关系。
第二个优点是 Join 层级少。
例如查询:
福建地区、黄金会员、手机品类的支付金额。
可以直接关联:
事实表
+ 用户维度
+ 地区维度
+ 商品维度
不会继续从商品维度关联分类,再从分类关联品牌。
第三个优点是业务含义清晰。
事实表负责记录业务事件,维度表负责提供分析角度,职责边界比较明确。
第四个优点是适合 OLAP 引擎。
Doris、StarRocks、ClickHouse 等系统更擅长大规模扫描和聚合。减少 Join 层级,通常有利于查询性能和执行计划稳定性。
6.5 星型模型的缺点
星型模型并不是没有代价。
由于维度表中会保存较多描述字段,因此可能存在:
- 数据冗余;
- 维度表字段较多;
- 属性更新时需要同步修改;
- 大维度维护成本较高。
例如同一个品牌名称可能在商品维度中重复出现很多次。
但在分析系统中,适度冗余通常是可以接受的。
因为数据仓库更关注:
- 查询效率;
- 使用便利;
- 口径统一;
- 模型可理解性。
而不是像业务数据库那样尽量消除所有冗余。
6.6 什么是雪花模型
雪花模型英文为 Snowflake Schema。
它是在星型模型基础上,将维度继续规范化拆分。
例如商品维度拆成:
fact_order_detail
|
↓
dim_product
|
↓
dim_category
|
↓
dim_brand
|
↓
dim_supplier
地区维度也可能拆成:
dim_city
↓
dim_province
↓
dim_country
完整结构可能是:
dim_date
|
|
dim_user —— fact_order_detail —— dim_product
| |
| ↓
dim_city dim_category
| |
↓ ↓
dim_province dim_brand
因为维度继续向外展开,结构像雪花,所以称为雪花模型。
6.7 雪花模型的优点
雪花模型更接近规范化设计。
主要优点包括:
- 减少维度属性重复;
- 维度层级关系更清晰;
- 独立维护品牌、品类、供应商等主数据;
- 某些公共维度更容易复用;
- 适合层级复杂、治理要求高的场景。
例如品牌信息由专门团队维护时,可以单独设计品牌维度。
商品维度只保存:
product_id
product_name
brand_id
category_id
品牌名称和品牌属性统一放在品牌维度中。
6.8 雪花模型的缺点
雪花模型的主要问题是查询复杂。
统计商品品类销售额时,可能需要:
事实表
↓
商品维度
↓
品类维度
统计品牌和供应商数据时,Join 层级会进一步增加。
这会带来:
- SQL 更复杂;
- 查询人员理解成本更高;
- Join 数量增加;
- 查询性能可能下降;
- 模型更像业务数据库;
- 自助分析难度提高。
如果维度拆分过度,最终会重新回到多表 Join 的问题。
6.9 星型模型和雪花模型如何选择
可以简单比较:
在大多数企业分析场景中,可以优先采用星型模型。
只有满足以下情况时,才考虑把部分维度雪花化:
- 维度层级非常复杂;
- 某个子维度需要被多个模型独立复用;
- 主数据由独立系统维护;
- 维度数据规模很大;
- 属性更新频率和权限边界明显不同。
实际项目不必在星型和雪花之间做绝对选择。
很多企业采用的是混合模型:
核心分析路径保持星型,少量复杂维度根据治理需要适当拆分。
6.10 星型模型不等于只有一张事实表
真实数据仓库通常有多张事实表。
例如电商交易域可能包括:
fact_order_detail
fact_payment
fact_refund
fact_delivery
这些事实表可以共享一致性维度:
dim_user
dim_product
dim_date
dim_region
dim_channel
形成多个相关星型模型。
例如:
dim_user
|
|
dim_product —— fact_order_detail —— dim_date
|
dim_region
支付事实表:
dim_user
|
|
dim_channel —— fact_payment —— dim_date
|
dim_region
退款事实表:
dim_user
|
|
dim_product —— fact_refund —— dim_date
|
dim_refund_reason
通过共享维度,可以跨业务过程分析。
例如:
- 下单后支付成功率;
- 支付后退款率;
- 不同用户等级的退款情况;
- 不同商品品类的支付转化。
七、维度发生变化怎么办:SCD 缓慢变化维
事实表会不断新增业务事件,维度表则用于描述业务背景。
但维度并不是永远不变。
例如:
- 用户从普通会员升级为黄金会员;
- 商品从手机分类调整到智能设备分类;
- 用户从福建搬到上海;
- 门店从华东区域调整到华南区域;
- 客户风险等级从低风险变为高风险。
这些变化会带来一个重要问题:
查询历史事实时,应该使用维度当前值,还是事实发生时的历史值?
这就是缓慢变化维需要解决的问题。
SCD 的全称是 Slowly Changing Dimension。
它用于处理维度属性随时间变化的问题。
7.1 为什么直接更新维度会有问题
假设用户10001在2026年1月是普通会员,3月升级为黄金会员。
用户维度最初是:
3月升级后直接更新为:
现在查询1月份订单:
SELECT
u.member_level,
SUM(f.pay_amount)
FROM fact_order_detail f
JOIN dim_user u
ON f.user_id = u.user_id
WHERE f.order_time >= '2026-01-01'
AND f.order_time < '2026-02-01'
GROUP BY u.member_level;
结果会把1月份的历史订单归为黄金会员订单。
但用户在1月份实际上还是普通会员。
这就是历史维度被当前值覆盖的问题。
7.2 SCD Type 1:直接覆盖
Type 1 的处理方式最简单:
直接用新值覆盖旧值,不保存历史。
例如:
NORMAL → GOLD
更新后只保留:
这种方式适合:
- 修正错误数据;
- 不需要历史分析的字段;
- 邮箱格式修正;
- 用户昵称修改;
- 拼写错误修正。
优点:
- 实现简单;
- 存储成本低;
- 查询方便。
缺点:
- 历史信息丢失;
- 无法还原事实发生时的维度状态。
因此,会员等级、组织归属、风险等级等重要分析属性通常不适合只使用 Type 1。
7.3 SCD Type 2:新增历史版本
Type 2 是数据仓库中最常用的历史维度处理方式。
它的核心思想是:
维度发生变化时,不覆盖旧记录,而是新增一个版本。
例如:
其中:
user_id是业务系统自然键;user_sk是数据仓库代理键;start_date表示版本生效时间;end_date表示版本失效时间;is_current表示是否为当前版本。
1月份订单关联:
user_sk = 900001
3月份之后的订单关联:
user_sk = 900002
这样就能保留完整历史。
7.4 为什么需要代理键
如果事实表只保存业务用户 ID:
user_id = 10001
那么同一个用户的多个历史版本都会拥有相同的 user_id。
事实表无法判断应该关联哪个版本。
因此,可以在维度表中生成代理键:
user_sk
例如:
900001:普通会员版本
900002:黄金会员版本
事实发生时,将对应版本的代理键写入事实表。
这样历史关联就不会受到后续维度变化影响。
7.5 Type 2 如何匹配历史版本
假设订单发生时间是:
2026-02-10
需要匹配满足下面条件的用户版本:
order_time >= start_date
AND order_time < end_date
示例:
SELECT
f.order_id,
f.order_time,
u.member_level
FROM fact_order_source f
JOIN dim_user_scd u
ON f.user_id = u.user_id
AND f.order_time >= u.start_date
AND f.order_time < u.end_date;
这类关联也叫时间区间关联。
在实时数仓中,还可以通过 Flink Temporal Join 或版本维表方式实现。
7.6 SCD Type 3:增加历史字段
Type 3 不新增记录,而是在同一行保存有限的历史状态。
例如:
优点是结构简单。
缺点是只能保存有限历史。
如果用户继续升级为钻石会员:
更早的普通会员状态就丢失了。
因此,Type 3 适合只关心当前值和上一个值的场景,但实际数据仓库中使用频率通常低于 Type 2。
7.7 哪些字段需要保留历史
不是所有维度字段都需要 Type 2。
可以按照分析价值区分。
适合 Type 1 的字段:
用户昵称
商品描述拼写
联系人电话修正
错误分类修复
适合 Type 2 的字段:
会员等级
客户风险等级
用户所属地区
商品所属分类
门店所属区域
员工所属部门
客户经理归属
判断标准是:
这个字段的历史变化是否会影响过去事实的业务解释?
如果会,就应该考虑保留历史。
7.8 商品价格应该放在哪里
很多人会把商品价格放进商品维度。
但价格变化频繁,而且订单发生时的成交价格属于业务事实。
因此需要区分:
- 当前商品标价,可以放在商品维度或商品快照中;
- 订单成交价格,必须保存在订单事实表中;
- 每日价格变化,可以设计价格快照事实表;
- 促销价格变化,可以设计价格变更事实表。
不能通过商品当前价格去还原历史订单金额。
事实表应该保存业务发生时已经确定的度量值。
7.9 实时场景中的维度更新
在 Flink 实时数仓中,维度可能存放在:
- MySQL;
- HBase;
- Redis;
- Doris;
- PostgreSQL;
- Flink State;
- Kafka Compact Topic。
事实流到达后,可以通过:
- Lookup Join;
- Temporal Join;
- Broadcast State;
- 异步 I/O;
补充维度属性。
例如订单事件中只有:
{
"order_id": 10001,
"user_id": 20001,
"product_id": 30001,
"pay_amount": 4500
}
通过维表关联后补充:
{
"order_id": 10001,
"user_id": 20001,
"member_level": "GOLD",
"province": "福建",
"product_id": 30001,
"category_name": "手机",
"brand_name": "Brand A",
"pay_amount": 4500
}
但要注意:
如果直接查询当前维表,可能会把当前维度状态关联到历史事实。
是否需要历史版本,取决于业务分析口径。
八、电商业务完整维度建模案例
下面按照 Kimball 四步法,为一个电商交易场景完成简化建模。
假设业务需求包括:
- 统计每天订单量和销售额;
- 分析不同商品品类销售表现;
- 分析不同用户等级消费情况;
- 分析地区和渠道贡献;
- 分析支付和退款情况;
- 计算订单履约时长。
8.1 第一步:选择业务过程
首先梳理核心业务过程:
提交订单
完成支付
发起退款
完成发货
签收商品
这些业务过程不应该全部塞进一张表。
可以分别设计:
fact_order_detail
fact_payment
fact_refund
fact_order_lifecycle
8.2 第二步:声明粒度
订单明细事实表
粒度:
一行代表一个订单中的一个商品明细。
主键:
order_detail_id
支付事实表
粒度:
一行代表一次支付请求。
主键:
payment_id
退款事实表
粒度:
一行代表一次退款申请明细。
主键:
refund_id
订单生命周期事实表
粒度:
一行代表一个订单从创建到完成的履约过程。
主键:
order_id
8.3 第三步:确定维度
公共维度包括:
dim_user
dim_product
dim_shop
dim_date
dim_time
dim_region
dim_channel
dim_activity
dim_payment_method
dim_refund_reason
不同事实表不一定关联所有维度。
例如订单明细事实可以关联:
用户
商品
店铺
日期
地区
渠道
活动
支付事实可以关联:
用户
日期
地区
渠道
支付方式
退款事实可以关联:
用户
商品
日期
退款原因
8.4 第四步:确定事实
订单明细事实表中的度量值:
商品数量
商品原价金额
优惠金额
实际支付金额
运费分摊金额
平台补贴金额
商家优惠金额
支付事实中的度量值:
支付请求金额
支付成功金额
支付手续费
退款事实中的度量值:
退款申请金额
实际退款金额
退货商品数量
订单生命周期事实中的度量值:
下单到支付时长
支付到发货时长
发货到签收时长
订单总履约时长
8.5 订单明细事实表设计
示例结构:
CREATE TABLE fact_order_detail (
order_detail_id BIGINT,
order_id BIGINT,
user_sk BIGINT,
product_sk BIGINT,
shop_sk BIGINT,
date_sk INT,
region_sk BIGINT,
channel_sk BIGINT,
activity_sk BIGINT,
product_quantity INT,
original_amount DECIMAL(18, 2),
discount_amount DECIMAL(18, 2),
pay_amount DECIMAL(18, 2),
shipping_amount DECIMAL(18, 2),
platform_subsidy_amount DECIMAL(18, 2),
merchant_discount_amount DECIMAL(18, 2),
order_status STRING,
order_time DATETIME,
PRIMARY KEY (order_detail_id)
);
粒度是一行一个订单商品明细,因此所有金额都应该是当前商品明细粒度上的金额。
整单金额不能未经处理直接重复写入每个明细行。
8.6 用户维度设计
示例:
CREATE TABLE dim_user (
user_sk BIGINT,
user_id BIGINT,
user_name STRING,
gender STRING,
age_group STRING,
member_level STRING,
register_channel STRING,
register_date DATE,
province_name STRING,
city_name STRING,
start_date DATE,
end_date DATE,
is_current INT,
PRIMARY KEY (user_sk)
);
如果会员等级、地区等属性需要保留历史,可以使用 Type 2。
如果昵称变化不需要历史,则可以直接覆盖。
8.7 商品维度设计
示例:
CREATE TABLE dim_product (
product_sk BIGINT,
product_id BIGINT,
product_name STRING,
sku_code STRING,
category_id BIGINT,
category_name STRING,
brand_id BIGINT,
brand_name STRING,
supplier_id BIGINT,
supplier_name STRING,
price_range STRING,
product_status STRING,
start_date DATE,
end_date DATE,
is_current INT,
PRIMARY KEY (product_sk)
);
商品维度可以适当冗余品类、品牌和供应商名称,减少分析查询中的 Join。
但历史订单金额仍然应该保存于事实表,不能依赖商品维度中的当前价格。
8.8 支付事实表设计
示例:
CREATE TABLE fact_payment (
payment_id BIGINT,
order_id BIGINT,
user_sk BIGINT,
date_sk INT,
region_sk BIGINT,
channel_sk BIGINT,
payment_method_sk BIGINT,
request_amount DECIMAL(18, 2),
success_amount DECIMAL(18, 2),
payment_fee DECIMAL(18, 2),
payment_status STRING,
request_time DATETIME,
success_time DATETIME,
PRIMARY KEY (payment_id)
);
一笔订单可能存在多次支付尝试,因此不能用 order_id 作为唯一主键。
支付成功率可以计算为:
SUM(
CASE WHEN payment_status = 'SUCCESS' THEN 1 ELSE 0 END
) / COUNT(*)
8.9 退款事实表设计
示例:
CREATE TABLE fact_refund (
refund_id BIGINT,
order_id BIGINT,
order_detail_id BIGINT,
user_sk BIGINT,
product_sk BIGINT,
date_sk INT,
refund_reason_sk BIGINT,
apply_refund_amount DECIMAL(18, 2),
actual_refund_amount DECIMAL(18, 2),
refund_quantity INT,
refund_status STRING,
apply_time DATETIME,
finish_time DATETIME,
PRIMARY KEY (refund_id)
);
退款率不建议直接作为可加事实保存。
更合理的是保存:
actual_refund_amount
pay_amount
查询时计算:
SUM(actual_refund_amount) / SUM(pay_amount)
8.10 订单生命周期事实表
示例:
CREATE TABLE fact_order_lifecycle (
order_id BIGINT,
user_sk BIGINT,
shop_sk BIGINT,
region_sk BIGINT,
channel_sk BIGINT,
create_time DATETIME,
pay_time DATETIME,
ship_time DATETIME,
receive_time DATETIME,
finish_time DATETIME,
pay_duration_seconds BIGINT,
ship_duration_seconds BIGINT,
delivery_duration_seconds BIGINT,
total_duration_seconds BIGINT,
current_status STRING,
PRIMARY KEY (order_id)
);
这是一张累积快照事实表。
随着订单状态推进,同一行会持续更新。
它适合分析订单履约效率,但不适合代替订单明细事实表。
8.11 业务矩阵
Kimball 建模中,可以使用业务过程与维度矩阵来梳理模型。
示例:
这个矩阵可以帮助识别:
- 哪些维度应该统一;
- 哪些维度可以被多个事实表共享;
- 哪些业务过程还没有建模;
- 不同主题是否存在口径冲突。
九、Flink 与 Doris 实时数仓中的建模实践
前面的内容主要讨论逻辑模型。
实际项目中,还需要将这些模型落到 Kafka、Flink、Doris 等技术组件中。
一个典型实时数仓链路可能是:
MySQL
↓
Flink CDC / Debezium
↓
Kafka ODS
↓
Flink SQL / DataStream
↓
DWD 事实流与维度关联
↓
Doris 明细表
↓
DWS 公共汇总
↓
ADS 报表与数据服务
9.1 事实表通常来自业务事件流
订单、支付、退款等事实通常来自持续变化的业务数据。
例如:
orders
payments
refunds
通过 Flink CDC 读取 MySQL Binlog,转换为动态表。
订单源表示例:
CREATE TABLE mysql_order_detail (
order_detail_id BIGINT,
order_id BIGINT,
user_id BIGINT,
product_id BIGINT,
shop_id BIGINT,
region_id BIGINT,
channel_id BIGINT,
quantity INT,
original_amount DECIMAL(18, 2),
discount_amount DECIMAL(18, 2),
pay_amount DECIMAL(18, 2),
order_status STRING,
order_time TIMESTAMP(3),
update_time TIMESTAMP(3),
PRIMARY KEY (order_detail_id) NOT ENFORCED
) WITH (
'connector' = 'mysql-cdc',
'hostname' = 'mysql',
'port' = '3306',
'username' = 'flink',
'password' = '123456',
'database-name' = 'demo',
'table-name' = 'order_detail'
);
Flink 会将 MySQL 的新增、更新和删除表示为动态表变更。
9.2 维度表通常来自主数据
用户、商品、地区、渠道等维度一般来自相对稳定的主数据表。
例如:
users
products
regions
channels
维度可以存放在 MySQL、Doris、HBase 或 Redis。
Flink 处理事实流时,再通过维表关联补充属性。
9.3 Lookup Join
如果维度表存放在外部数据库中,可以使用 Lookup Join。
示例:
SELECT
o.order_detail_id,
o.order_id,
o.user_id,
u.member_level,
u.province_name,
o.product_id,
p.category_name,
p.brand_name,
o.pay_amount,
o.order_time
FROM mysql_order_detail o
LEFT JOIN dim_user FOR SYSTEM_TIME AS OF o.proc_time AS u
ON o.user_id = u.user_id
LEFT JOIN dim_product FOR SYSTEM_TIME AS OF o.proc_time AS p
ON o.product_id = p.product_id;
Lookup Join 的特点是:
- 事实流每来一条数据;
- 根据主键查询维度;
- 将维度属性补充到事实记录中。
需要注意缓存、超时和外部数据库压力。
9.4 Temporal Join
如果维度表也是动态变化的,并且需要按照事实发生时的版本关联,可以使用 Temporal Join。
它能够表达:
订单发生在某个时刻时,用户或商品维度是什么状态。
这比直接关联当前维度更适合历史分析。
但实际使用时需要确认:
- 维度是否有版本字段;
- 时间属性是否正确;
- 上游更新顺序是否可靠;
- 是否需要处理迟到数据。
9.5 是否应该在 Flink 中直接做大宽表
Flink 可以把事实流与多张维度表关联,生成宽表写入 Doris。
例如订单宽表包含:
订单字段
用户等级
用户地区
商品品类
商品品牌
店铺类型
渠道名称
活动名称
优点是:
- Doris 查询简单;
- 减少运行时 Join;
- BI 使用方便;
- 实时报表响应快。
但也存在问题:
- 维度更新后历史宽表是否同步;
- 字段越来越多;
- 多次 Lookup 增加延迟;
- 外部维度查询压力较大;
- 宽表容易绑定特定需求。
因此,推荐做法是:
对高频公共分析场景构建适度宽表,不要追求一张覆盖所有业务的万能宽表。
9.6 Doris 中事实表如何选表模型
以订单明细为例,如果每个 order_detail_id 只保留最新状态,可以考虑主键模型或 Unique Key 模型。
例如:
CREATE TABLE dwd_order_detail (
order_detail_id BIGINT,
order_id BIGINT,
user_id BIGINT,
product_id BIGINT,
category_name STRING,
brand_name STRING,
member_level STRING,
region_name STRING,
quantity INT,
pay_amount DECIMAL(18, 2),
order_status STRING,
update_time DATETIME
)
UNIQUE KEY(order_detail_id)
DISTRIBUTED BY HASH(order_detail_id) BUCKETS 8;
MySQL 中订单明细发生更新时,Flink CDC 可以将更新写入 Doris。
Doris 根据唯一键更新对应记录。
如果需要保存完整状态变更历史,则应该另建事件明细表,而不是只保留最新状态。
9.7 Doris 中的维度表
小型维度可以直接存入 Doris,例如:
dim_region
dim_channel
dim_date
查询时再关联。
对于频繁查询的维度属性,也可以冗余到事实宽表中。
选择取决于:
- 维度大小;
- 更新频率;
- 查询频率;
- Join 性能;
- 历史版本需求;
- 数据一致性要求。
9.8 实时 DWD 与 DWS
实时 DWD 主要负责:
- 清洗;
- 去重;
- 标准化;
- 业务口径计算;
- 维度补充;
- 生成标准事实。
实时 DWS 主要负责:
- 按分钟、小时、天聚合;
- 生成用户、商品、城市等主题汇总;
- 为实时大屏和接口提供结果。
例如:
SELECT
window_start,
window_end,
region_name,
category_name,
COUNT(DISTINCT order_id) AS order_count,
SUM(pay_amount) AS gmv
FROM TABLE(
TUMBLE(
TABLE dwd_order_detail,
DESCRIPTOR(order_time),
INTERVAL '1' MINUTE
)
)
GROUP BY
window_start,
window_end,
region_name,
category_name;
这类结果可以写入 Doris 的实时汇总表。
9.9 常见建模错误
错误一:没有先声明粒度
一张表同时混合订单、商品和支付粒度。
结果是金额重复、订单数错误、Join 膨胀。
错误二:事实表中保存大量不可解释的汇总值
例如只保存退款率,不保存退款金额和支付金额。
后续无法重新计算和校验。
错误三:所有维度都拆成雪花模型
虽然结构规范,但查询复杂,最终没人愿意使用。
错误四:所有字段都塞进宽表
宽表越来越大,维度更新和任务维护成本不断上升。
错误五:使用当前维度解释历史事实
用户当前是黄金会员,不代表历史订单发生时也是黄金会员。
错误六:不同事实表使用不同维度定义
订单主题和支付主题分别定义用户等级,导致跨主题指标不一致。
错误七:把系统表当成业务过程
根据 MySQL 表一对一复制,而没有识别下单、支付、退款等真正业务事实。
错误八:事实表只保留最终状态
如果业务需要分析状态流转,就必须保留事件历史或单独建设状态变更事实表。
总结
维度建模不是简单地把业务表拆成事实表和维度表。
它真正解决的是:
- 企业应该围绕什么业务过程组织数据;
- 一行数据到底代表什么;
- 哪些字段属于可计算事实;
- 哪些字段属于分析维度;
- 历史维度变化如何保留;
- 不同业务过程如何共享统一维度;
- 模型如何在 Flink、Kafka 和 Doris 中落地。
Kimball 维度建模可以概括为四步:
选择业务过程
↓
声明粒度
↓
确定维度
↓
确定事实
其中,最重要的是声明粒度。
事实表负责回答:
发生了什么业务事件?
维度表负责回答:
这件事是在什么时间、地点、用户、商品和渠道背景下发生的?
星型模型通过较少的 Join,提供简单清晰的分析结构。
雪花模型通过继续拆分维度,提高规范性和独立治理能力。
SCD 则负责保存维度随时间变化的历史状态。
在实时数仓中,典型落地链路是:
MySQL
↓
Flink CDC
↓
Kafka / ODS
↓
Flink 清洗、去重、维度关联
↓
DWD 事实模型
↓
DWS 公共汇总
↓
Doris / ADS / BI
真正好的数据模型,不是表越多越专业,也不是字段越宽越先进,而是能够做到:
粒度清晰、口径统一、历史可追溯、模型可复用、指标可解释。
下一篇可以继续深入: