数据仓库工具箱 (持续更新)
第一章:维度建模基础
1.1 维度建模的核心目标
两大核心目标:
-
以商业用户可理解的方式发布数据
- 使用业务术语而非技术术语
- 结构直观,易于理解
- 与业务流程一致
-
提供高效的数据查询性能
- 最小化表连接数量
- 优化查询路径
- 支持快速聚合计算
1.2 设计原则
简单性原则:
简单数据模型 → 易于理解 → 高效查询
↓
复杂数据模型 → 难以理解 → 性能低下
关键要点:
- ✅ 从简单的数据模型开始
- ✅ 不要求必须满足第三范式(3NF)
- ❌ 避免过度复杂的模型设计
3NF vs 维度模型对比:
| 特性 | 3NF(规范化) | 维度模型 |
|---|---|---|
| 设计目标 | 消除数据冗余 | 查询性能和易用性 |
| 表的数量 | 多(高度拆分) | 少(星型结构) |
| 查询复杂度 | 高(多表JOIN) | 低(少量JOIN) |
| 数据冗余 | 最小 | 适度允许 |
| 适用场景 | OLTP(事务处理) | OLAP(分析处理) |
1.3 星型模型 (Star Schema)
定义:维度模型的基本结构,由一个事实表和多个维度表组成,形似星状。
结构图:
维度表1
|
维度表2 - 事实表 - 维度表3
|
维度表4
组成部分:
1.3.1 事实表 (Fact Table)
特征:
- 存储业务过程的度量值(数值型数据)
- 位于星型结构的中心
- 包含外键连接到维度表
- 包含可加性的事实(度量值)
- 行数通常非常大(百万到数十亿行)
示例:销售事实表
销售事实表
├── 日期键 (FK)
├── 产品键 (FK)
├── 门店键 (FK)
├── 促销键 (FK)
├── 销售数量 (度量)
├── 销售额 (度量)
└── 折扣金额 (度量)
1.3.2 维度表 (Dimension Table)
特征:
- 包含描述性文本信息
- 提供业务上下文
- 通常包含 50-100 个属性
- 行数相对较少
- 有单一主键
示例:产品维度表
产品维度表
├── 产品键 (PK)
├── 产品名称
├── 品牌
├── 类别
├── 子类别
├── 包装类型
├── 包装尺寸
├── 重量
└── ... (更多属性)
1.4 事实表粒度
粒度定义:事实表中一行所表示的业务含义的详细程度
三种粒度类型:
1.4.1 事务粒度 (Transaction Grain)
- 最细粒度
- 每行代表一次业务事件
- 示例:每行 = 一笔销售交易
1.4.2 周期快照粒度 (Periodic Snapshot Grain)
- 汇总粒度
- 每行代表某个时间段的汇总
- 示例:每行 = 某一天的库存快照
1.4.3 累积快照粒度 (Accumulating Snapshot Grain)
- 过程粒度
- 每行代表业务流程的生命周期
- 示例:每行 = 一个订单从下单到完成的全过程
粒度选择原则:
粒度越细 → 灵活性越高 → 存储空间越大
粒度越粗 → 查询越快 → 灵活性越低
1.5 雪花模型 (Snowflake Schema)
定义:在星型模型基础上,将维度表进一步规范化,形成多层次结构。
结构对比:
星型模型:
产品维度表
├── 产品键
├── 产品名称
├── 品牌名称
├── 类别名称
└── 子类别名称
雪花模型:
产品维度表 品牌维度表
├── 产品键 ├── 品牌键
├── 产品名称 → ├── 品牌名称
├── 品牌键 (FK) └── 品牌描述
└── 子类别键 (FK)
↓
子类别维度表 类别维度表
├── 子类别键 ├── 类别键
├── 子类别名称 → ├── 类别名称
└── 类别键 (FK) └── 类别描述
星型 vs 雪花对比:
| 特性 | 星型模型 | 雪花模型 |
|---|---|---|
| 维度表规范化 | 否(反规范化) | 是 |
| 数据冗余 | 较多 | 较少 |
| 查询性能 | 更好(JOIN少) | 较差(JOIN多) |
| 维护复杂度 | 低 | 高 |
| 存储空间 | 略大 | 略小 |
| 推荐度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
重要结论:
- ❌ 雪花模型不推荐用于数据仓库
- ✅ 维度表的数据量相对较小,规范化节省空间有限
- ✅ 星型模型的查询性能优势远大于空间节省
1.6 数据集市 (Data Mart)
定义:面向特定业务部门或主题的小型数据仓库。
特点:
- 从企业级数据仓库中提取特定数据
- 为特定用户群体服务
- 提供快速查询和分析
- 数据量相对较小
常见数据集市类型:
| 类型 | 主要内容 | 服务对象 |
|---|---|---|
| 销售集市 | 销售额、客户、订单、产品 | 销售部门 |
| 财务集市 | 收入、成本、利润、预算 | 财务部门 |
| 营销集市 | 活动效果、客户行为、ROI | 市场部门 |
| 人力集市 | 员工、薪资、绩效、考勤 | 人力资源 |
| 供应链集市 | 库存、采购、物流、供应商 | 供应链部门 |
架构关系:
企业数据仓库 (EDW)
├── 销售数据集市
├── 财务数据集市
├── 营销数据集市
└── 库存数据集市
第二章:维度建模技术
2.1 维度模型设计的四步骤
Kimball 四步骤法:
1. 选择业务过程
↓
2. 声明粒度
↓
3. 确认维度
↓
4. 确认事实
步骤 1:选择业务过程
定义:确定要建模的业务活动
示例:
- 销售下单
- 库存盘点
- 客户投诉
- 网站访问
关键问题:
- 这个业务过程产生什么业务事件?
- 业务用户关心哪些业务过程?
- 数据源能支持哪些业务过程?
步骤 2:声明粒度
定义:明确事实表中一行所代表的业务含义
粒度示例:
- ✅ 细粒度:每行 = 一个销售订单中的一个商品行项
- ⚠️ 中粒度:每行 = 某一天某个门店的销售汇总
- ❌ 粗粒度:每行 = 某个月某个区域的销售汇总
重要原则:
- 📌 粒度是维度模型设计中最关键的决策
- 📌 粒度决定了维度的选择
- 📌 建议从最细粒度开始设计
步骤 3:确认维度
定义:识别描述业务过程上下文的"谁、什么、哪里、何时、为什么、如何"
维度识别检查清单:
- 何时 (When):日期、时间
- 谁 (Who):客户、员工、供应商
- 什么 (What):产品、服务
- 哪里 (Where):地点、门店、仓库
- 为什么 (Why):原因、促销
- 如何 (How):渠道、方式
示例:销售业务过程
维度列表:
├── 日期维度(何时)
├── 产品维度(什么)
├── 门店维度(哪里)
├── 客户维度(谁)
├── 促销维度(为什么)
└── 支付方式维度(如何)
步骤 4:确认事实
定义:识别业务过程产生的数值型度量
事实类型:
- 可加事实 - 可以跨所有维度求和
- 半可加事实 - 不能跨时间维度求和(如库存)
- 不可加事实 - 不能求和(如比率、百分比)
示例:销售事实
可加事实:
├── 销售数量
├── 销售额
├── 折扣金额
├── 成本
└── 利润
半可加事实:
└── 库存数量(不能跨时间求和)
不可加事实:
├── 单价(比率)
└── 利润率(百分比)
2.2 一致性事实 (Conformed Facts)
定义:在不同事实表中,相同业务含义的事实必须有相同的技术定义和命名。
重要性:
- 确保跨事实表的可比性
- 避免数据歧义
- 支持钻取分析
一致性规则:
| 场景 | 处理方式 |
|---|---|
| 相同业务含义 + 相同计算逻辑 | ✅ 使用相同命名 |
| 相同业务含义 + 不同计算逻辑 | ❌ 使用不同命名 |
| 不同业务含义 | ❌ 必须使用不同命名 |
示例:
✅ 正确:
销售事实表.销售额 = 数量 × 单价
退货事实表.销售额 = 数量 × 单价
(定义一致,命名一致)
❌ 错误:
销售事实表.销售额 = 数量 × 单价
预测事实表.销售额 = 基于模型的预测值
(定义不一致,但命名相同 - 会导致混淆)
✅ 正确修正:
销售事实表.实际销售额
预测事实表.预测销售额
(定义不同,命名不同)
2.3 一致性维度 (Conformed Dimensions)
定义:在多个事实表中共享的维度,具有相同的属性和含义。
示例:
日期维度 → 被销售、库存、采购事实表共享
产品维度 → 被销售、库存、退货事实表共享
客户维度 → 被销售、服务、营销事实表共享
实现方式:
- 物理一致性:多个事实表使用同一个维度表
- 逻辑一致性:不同维度表但属性定义一致
好处:
- ✅ 支持跨业务过程的钻取
- ✅ 确保分析的一致性
- ✅ 简化 ETL 开发
- ✅ 提高用户信任度
第三章:事实表类型详解
3.1 事务事实表 (Transaction Fact Table)
定义:记录业务事件发生时点的度量,每行对应一次业务事务。
特征:
- ✅ 粒度最细 - 原子级别
- ✅ 最常见 - 80% 的事实表是事务型
- ✅ 可稀疏 - 只有事件发生时才有记录
- ✅ 最灵活 - 支持任意维度的分析
典型应用场景:
- 销售交易
- ATM 取款
- 网站点击
- 呼叫中心通话记录
表结构示例:销售事务事实表
CREATE TABLE fact_sales (
-- 维度外键
date_key INT NOT NULL,
product_key INT NOT NULL,
store_key INT NOT NULL,
customer_key INT NOT NULL,
promotion_key INT NOT NULL,
-- 退化维度
transaction_number VARCHAR(20) NOT NULL,
-- 度量
quantity_sold DECIMAL(10,2),
sales_amount DECIMAL(12,2),
discount_amount DECIMAL(12,2),
cost_amount DECIMAL(12,2),
profit_amount DECIMAL(12,2),
-- 时间戳
transaction_timestamp TIMESTAMP,
PRIMARY KEY (date_key, product_key, store_key, transaction_number)
);
数据示例:
| 日期键 | 产品键 | 门店键 | 交易号 | 销售数量 | 销售额 |
|---|---|---|---|---|---|
| 20240101 | 1001 | 5 | TXN001 | 2 | 200.00 |
| 20240101 | 1002 | 5 | TXN001 | 1 | 150.00 |
| 20240101 | 1003 | 7 | TXN002 | 5 | 500.00 |
优点:
- 📈 支持任意粒度的汇总
- 📈 保留完整的历史细节
- 📈 灵活性最高
缺点:
- 📉 数据量最大
- 📉 查询汇总时需要实时计算
3.2 周期快照事实表 (Periodic Snapshot Fact Table)
定义:在预定的时间间隔(如每天、每周、每月)记录度量的累计值。
特征:
- ✅ 固定周期 - 每个周期一行
- ✅ 可预测 - 行数 = 周期数 × 维度组合数
- ✅ 稠密表 - 即使没有活动也会有记录
- ✅ 汇总性 - 已经预先汇总
典型应用场景:
- 每日库存快照
- 每月账户余额
- 每周业绩汇总
- 每日网站流量统计
表结构示例:每日库存快照事实表
CREATE TABLE fact_inventory_snapshot (
-- 维度外键
date_key INT NOT NULL,
product_key INT NOT NULL,
warehouse_key INT NOT NULL,
-- 半可加度量(不能跨时间求和)
quantity_on_hand DECIMAL(10,2),
quantity_reserved DECIMAL(10,2),
quantity_available DECIMAL(10,2),
-- 可加度量
inventory_value DECIMAL(12,2),
-- 不可加度量
days_of_supply INT,
turnover_rate DECIMAL(5,2),
PRIMARY KEY (date_key, product_key, warehouse_key)
);
数据示例:
| 日期 | 产品键 | 仓库键 | 现有库存 | 库存价值 | 周转率 |
|---|---|---|---|---|---|
| 2024-01-01 | 1001 | 1 | 500 | 50000 | 2.5 |
| 2024-01-02 | 1001 | 1 | 480 | 48000 | 2.6 |
| 2024-01-03 | 1001 | 1 | 520 | 52000 | 2.4 |
与事务表的对比:
| 特性 | 事务事实表 | 周期快照事实表 |
|---|---|---|
| 粒度 | 最细(每笔交易) | 周期汇总 |
| 行数 | 最多 | 中等 |
| 数据密度 | 稀疏 | 稠密 |
| 查询性能 | 需要聚合 | 已预聚合 |
| 适用场景 | 详细分析 | 趋势分析 |
优点:
- 📈 查询性能好(已预聚合)
- 📈 便于趋势分析
- 📈 行数可预测
缺点:
- 📉 丢失细节信息
- 📉 灵活性较低
- 📉 某些度量不可加(如库存)
3.3 累积快照事实表 (Accumulating Snapshot Fact Table)
定义:记录具有明确开始和结束的业务流程,一行代表整个流程的生命周期。
特征:
- ✅ 流程导向 - 关注业务流程的各个阶段
- ✅ 多个日期 - 包含多个日期维度(每个里程碑一个)
- ✅ 可更新 - 随流程推进不断更新
- ✅ SCD Type 1 - 覆盖更新而非保留历史
典型应用场景:
- 订单履行流程
- 制造流程
- 理赔流程
- 招聘流程
表结构示例:订单履行累积快照
CREATE TABLE fact_order_fulfillment (
-- 维度外键
order_key INT NOT NULL,
product_key INT NOT NULL,
customer_key INT NOT NULL,
-- 多个日期维度(流程各阶段)
order_date_key INT,
payment_date_key INT,
shipment_date_key INT,
delivery_date_key INT,
-- 时间间隔度量(滞后指标)
payment_lag_days INT,
shipment_lag_days INT,
delivery_lag_days INT,
total_fulfillment_days INT,
-- 其他度量
order_amount DECIMAL(12,2),
shipping_cost DECIMAL(12,2),
-- 流程状态
current_status VARCHAR(20),
PRIMARY KEY (order_key, product_key)
);
生命周期示例:
时间点 1:订单创建
| 订单键 | 下单日期 | 支付日期 | 发货日期 | 交付日期 | 订单金额 | 状态 |
|---|---|---|---|---|---|---|
| 1001 | 20240101 | NULL | NULL | NULL | 500.00 | 已下单 |
时间点 2:支付完成
| 订单键 | 下单日期 | 支付日期 | 发货日期 | 交付日期 | 订单金额 | 状态 |
|---|---|---|---|---|---|---|
| 1001 | 20240101 | 20240102 | NULL | NULL | 500.00 | 已支付 |
时间点 3:发货
| 订单键 | 下单日期 | 支付日期 | 发货日期 | 交付日期 | 订单金额 | 状态 |
|---|---|---|---|---|---|---|
| 1001 | 20240101 | 20240102 | 20240103 | NULL | 500.00 | 已发货 |
时间点 4:交付完成
| 订单键 | 下单日期 | 支付日期 | 发货日期 | 交付日期 | 订单金额 | 状态 |
|---|---|---|---|---|---|---|
| 1001 | 20240101 | 20240102 | 20240103 | 20240105 | 500.00 | 已完成 |
关键特性:滞后指标
-- 计算各阶段耗时
payment_lag_days = payment_date - order_date
shipment_lag_days = shipment_date - payment_date
delivery_lag_days = delivery_date - shipment_date
total_fulfillment_days = delivery_date - order_date
三种事实表类型对比:
| 特性 | 事务事实表 | 周期快照 | 累积快照 |
|---|---|---|---|
| 粒度 | 一次事务 | 一个周期 | 一个流程 |
| 时间维度 | 1个 | 1个 | 多个 |
| 更新方式 | 只插入 | 只插入 | 插入+更新 |
| 行数 | 最多 | 中等 | 最少 |
| 典型应用 | 销售、点击 | 库存、余额 | 订单、理赔 |
| 分析重点 | 详细活动 | 状态趋势 | 流程效率 |
3.4 无事实的事实表 (Factless Fact Table)
定义:只记录维度之间的关系,不包含数值型度量的事实表。
两种类型:
类型 1:事件跟踪表
记录事件的发生,即使没有可度量的数字。
示例:学生选课事实表
CREATE TABLE fact_student_enrollment (
date_key INT NOT NULL,
student_key INT NOT NULL,
course_key INT NOT NULL,
instructor_key INT NOT NULL,
classroom_key INT NOT NULL,
-- 无数值度量!
-- 只记录"发生了选课这个事件"
PRIMARY KEY (date_key, student_key, course_key)
);
应用场景:
- 学生选课记录
- 员工出勤记录
- 促销活动覆盖
- 产品展示事件
分析用途:
-- 虽然没有度量,但可以:
-- 1. 统计选课人数
SELECT course_key, COUNT(*) as student_count
FROM fact_student_enrollment
GROUP BY course_key;
-- 2. 统计某学生选了几门课
SELECT student_key, COUNT(*) as course_count
FROM fact_student_enrollment
GROUP BY student_key;
-- 3. 分析选课模式
SELECT course_key, COUNT(DISTINCT student_key)
FROM fact_student_enrollment
GROUP BY course_key;
类型 2:覆盖表
记录应该发生但可能没发生的事件。
示例:促销覆盖事实表
CREATE TABLE fact_promotion_coverage (
date_key INT NOT NULL,
product_key INT NOT NULL,
store_key INT NOT NULL,
promotion_key INT NOT NULL,
-- 记录"哪些产品在哪些门店参与了促销"
-- 即使没有销售也要记录
PRIMARY KEY (date_key, product_key, store_key, promotion_key)
);
分析用途:
-- 分析促销效果:参与促销但未产生销售的情况
SELECT pc.product_key, pc.store_key
FROM fact_promotion_coverage pc
LEFT JOIN fact_sales s
ON pc.date_key = s.date_key
AND pc.product_key = s.product_key
AND pc.store_key = s.store_key
WHERE s.sales_amount IS NULL;
-- 找出参与促销但没有销售的产品
无事实表的重要性:
- 📌 记录"没有发生"的事件(分母)
- 📌 支持比率和百分比计算
- 📌 完整的覆盖分析
第四章:维度表设计技术
4.1 维度表的特征
核心特征:
- 📝 宽表 - 通常包含 50-100 个属性
- 📝 少行 - 相比事实表行数少得多
- 📝 文本为主 - 大多数列是描述性文本
- 📝 单主键 - 通常是代理键(整数)
4.2 代理键 (Surrogate Key)
定义:数据仓库分配的整数主键,与业务键无关。
为什么使用代理键?
| 业务键的问题 | 代理键的优势 |
|---|---|
| 可能变化 | 永不改变 |
| 可能是组合键 | 单一整数 |
| 可能是字符串 | 整数(JOIN快) |
| 可能为空 | 永不为空 |
| 可能很长 | 固定长度 |
示例对比:
❌ 使用业务键(不推荐)
CREATE TABLE dim_product (
product_code VARCHAR(20) PRIMARY KEY, -- 业务键
product_name VARCHAR(100),
...
);
CREATE TABLE fact_sales (
product_code VARCHAR(20), -- 字符串JOIN,慢
...
);
✅ 使用代理键(推荐)
CREATE TABLE dim_product (
product_key INT PRIMARY KEY, -- 代理键(整数)
product_code VARCHAR(20), -- 业务键作为属性
product_name VARCHAR(100),
...
);
CREATE TABLE fact_sales (
product_key INT, -- 整数JOIN,快
...
);
4.3 退化维度 (Degenerate Dimension)
定义:存储在事实表
使用场景:
- 追溯原始交易
- 关联相关行项
- 问题排查
示例查询:
-- 查询某个订单的所有行项
SELECT *
FROM fact_sales
WHERE invoice_number = 'INV20240101001';
-- 统计每个订单的总金额
SELECT invoice_number, SUM(sales_amount) as order_total
FROM fact_sales
GROUP BY invoice_number;
4.4 维度层次结构 (Dimension Hierarchies)
定义:维度内部的上下级关系,支持钻取分析。
常见层次结构:
地理层次
国家
└── 省份
└── 城市
└── 门店
产品层次
产品类别
└── 子类别
└── 品牌
└── 产品
时间层次
年
└── 季度
└── 月
└── 周
└── 日
实现方式:扁平化存储(推荐)
CREATE TABLE dim_geography (
geography_key INT PRIMARY KEY,
-- 层次结构的所有级别都存储在同一表中
store_code VARCHAR(20),
store_name VARCHAR(100),
city_name VARCHAR(50),
province_name VARCHAR(50),
country_name VARCHAR(50),
-- 其他属性
store_type VARCHAR(20),
store_size_sqm INT
);
优点:
- ✅ 查询简单(无需JOIN)
- ✅ 性能好
- ✅ 易于理解
⚠️ 避免雪花化: 不要为每个层次创建单独的表!
4.5 缓慢变化维度 (Slowly Changing Dimensions, SCD)
定义:处理维度属性随时间变化的技术。
SCD Type 0:保留原始值
策略:永不改变 应用:原始值、固定参考数据
SCD Type 1:覆盖
策略:直接更新,不保留历史 应用:错误修正、不重要的变化
示例:
-- 客户地址变更(不保留历史)
UPDATE dim_customer
SET address = '新地址'
WHERE customer_key = 123;
优点:简单 缺点:丢失历史,无法回溯分析
SCD Type 2:增加新行(最常用)⭐
策略:插入新行,保留历史记录
表结构:
CREATE TABLE dim_customer (
customer_key INT PRIMARY KEY, -- 代理键
customer_id VARCHAR(20), -- 业务键
customer_name VARCHAR(100),
address VARCHAR(200),
customer_type VARCHAR(20),
-- SCD Type 2 控制字段
effective_date DATE, -- 生效日期
expiration_date DATE, -- 失效日期
is_current BOOLEAN, -- 是否当前版本
version_number INT -- 版本号
);
变化过程示例:
2023-01-01:初始记录
| customer_key | customer_id | name | address | effective_date | expiration_date | is_current |
|---|---|---|---|---|---|---|
| 1001 | C001 | 张三 | 北京 | 2023-01-01 | 9999-12-31 | TRUE |
2024-06-01:地址变更为上海
| customer_key | customer_id | name | address | effective_date | expiration_date | is_current |
|---|---|---|---|---|---|---|
| 1001 | C001 | 张三 | 北京 | 2023-01-01 | 2024-05-31 | FALSE |
| 1002 | C001 | 张三 | 上海 | 2024-06-01 | 9999-12-31 | TRUE |
优点:
- ✅ 完整保留历史
- ✅ 支持时点查询
- ✅ 最准确的历史分析
缺点:
- 📉 维度表行数增加
- 📉 ETL 复杂度提高
查询示例:
-- 查询当前版本
SELECT * FROM dim_customer WHERE is_current = TRUE;
-- 查询历史某个时点的状态
SELECT *
FROM dim_customer
WHERE customer_id = 'C001'
AND '2024-01-01' BETWEEN effective_date AND expiration_date;
SCD Type 3:增加新列
策略:增加列存储历史值(通常只保留前一个值)
表结构:
CREATE TABLE dim_customer (
customer_key INT PRIMARY KEY,
customer_id VARCHAR(20),
customer_name VARCHAR(100),
-- 当前值
current_address VARCHAR(200),
current_customer_type VARCHAR(20),
-- 前一个值
previous_address VARCHAR(200),
previous_customer_type VARCHAR(20),
-- 变更日期
last_change_date DATE
);
优点:简单,能比较前后变化 缺点:只能保留有限历史(通常一个版本)
应用场景:需要比较"前后"变化的分析
SCD Type 4:历史表
策略:当前值在主表,历史值在单独的历史表
主表(当前值):
CREATE TABLE dim_customer_current (
customer_key INT PRIMARY KEY,
customer_id VARCHAR(20),
customer_name VARCHAR(100),
address VARCHAR(200)
);
历史表:
CREATE TABLE dim_customer_history (
customer_key INT,
customer_id VARCHAR(20),
customer_name VARCHAR(100),
address VARCHAR(200),
effective_date DATE,
expiration_date DATE,
PRIMARY KEY (customer_key, effective_date)
);
SCD Type 6:混合型(Type 1 + 2 + 3)
策略:结合多种类型的优点
表结构:
CREATE TABLE dim_customer (
customer_key INT PRIMARY KEY,
customer_id VARCHAR(20),
customer_name VARCHAR(100),
-- Type 2:历史行
historical_address VARCHAR(200),
-- Type 3:当前值列
current_address VARCHAR(200),
-- Type 1:可覆盖的属性
email VARCHAR(100),
effective_date DATE,
expiration_date DATE,
is_current BOOLEAN
);
SCD 类型选择指南:
| SCD 类型 | 使用场景 | 推荐度 |
|---|---|---|
| Type 0 | 永不改变的数据(出生日期) | ⭐⭐⭐ |
| Type 1 | 错误修正、不重要的变化 | ⭐⭐⭐⭐ |
| Type 2 | 需要完整历史的重要属性 | ⭐⭐⭐⭐⭐ |
| Type 3 | 只需要前后对比 | ⭐⭐⭐ |
| Type 4 | 历史查询频率低 | ⭐⭐ |
| Type 6 | 复杂需求 | ⭐⭐ |
4.6 角色扮演维度 (Role-Playing Dimensions)
定义:同一个维度表在事实表中扮演不同角色。
最常见:日期维度
CREATE TABLE fact_order (
order_key INT,
-- 同一个日期维度,三种不同角色
order_date_key INT, -- 下单日期
ship_date_key INT, -- 发货日期
delivery_date_key INT, -- 交付日期
-- 度量
order_amount DECIMAL(12,2)
);
-- 都指向同一个日期维度表
CREATE TABLE dim_date (
date_key INT PRIMARY KEY,
full_date DATE,
year INT,
quarter INT,
month INT,
day_of_week VARCHAR(10)
);
实现方式:
- 物理方式:创建视图(推荐)
CREATE VIEW dim_order_date AS SELECT * FROM dim_date;
CREATE VIEW dim_ship_date AS SELECT * FROM dim_date;
CREATE VIEW dim_delivery_date AS SELECT * FROM dim_date;
- 逻辑方式:在BI工具中创建别名
其他示例:
- 发货地址 vs 收货地址(同一地理维度)
- 发起人 vs 审批人(同一员工维度)
- 起点 vs 终点(同一地点维度)
4.7 杂项维度 (Junk Dimensions)
定义:将多个低基数的标志和指示器组合到一个维度表中。
问题:事实表中的多个标志字段
-- ❌ 不推荐:事实表中有太多标志字段
CREATE TABLE fact_sales (
date_key INT,
product_key INT,
-- 太多标志字段!
is_weekend BOOLEAN,
is_holiday BOOLEAN,
is_promotion BOOLEAN,
is_cash_payment BOOLEAN,
is_member BOOLEAN,
weather_condition VARCHAR(20),
sales_amount DECIMAL(12,2)
);
解决方案:创建杂项维度
-- ✅ 推荐:杂项维度表
CREATE TABLE dim_transaction_profile (
profile_key INT PRIMARY KEY,
is_weekend BOOLEAN,
is_holiday BOOLEAN,
is_promotion BOOLEAN,
is_cash_payment BOOLEAN,
is_member BOOLEAN,
weather_condition VARCHAR(20)
);
-- 简化的事实表
CREATE TABLE fact_sales (
date_key INT,
product_key INT,
profile_key INT, -- 指向杂项维度
sales_amount DECIMAL(12,2)
);
杂项维度的组合: 假设 5 个布尔标志 + 1 个天气(3种可能)
- 最多组合数:2^5 × 3 = 96 行
优点:
- ✅ 减少事实表列数
- ✅ 便于分析标志组合
- ✅ 提高查询性能
4.8 日期维度设计
日期维度是最重要的维度之一
完整的日期维度表结构:
CREATE TABLE dim_date (
date_key INT PRIMARY KEY, -- 格式:20240101
-- 基本日期信息
full_date DATE,
date_name VARCHAR(20), -- 2024年1月1日
-- 年相关
year INT,
year_name VARCHAR(10), -- 2024年
-- 季度相关
quarter INT,
quarter_name VARCHAR(10), -- 2024-Q1
-- 月相关
month INT,
month_name VARCHAR(20), -- 2024年1月
month_abbr VARCHAR(3), -- Jan
-- 周相关
week_of_year INT,
iso_week INT,
-- 日相关
day_of_month INT,
day_of_year INT,
day_of_week INT, -- 1=周一
day_name VARCHAR(10), -- 星期一
day_abbr VARCHAR(3), -- 周一
-- 业务属性
is_weekend BOOLEAN,
is_holiday BOOLEAN,
holiday_name VARCHAR(50),
is_working_day BOOLEAN,
-- 财务周期
fiscal_year INT,
fiscal_quarter INT,
fiscal_month INT,
-- 相对日期(用于相对时间分析)
days_from_today INT,
weeks_from_today INT,
-- 同期分析
same_day_last_year_key INT,
same_day_last_month_key INT
);
数据示例:
| date_key | full_date | year | quarter | month | day_name | is_weekend | is_holiday |
|---|---|---|---|---|---|---|---|
| 20240101 | 2024-01-01 | 2024 | 1 | 1 | 星期一 | FALSE | TRUE |
| 20240102 | 2024-01-02 | 2024 | 1 | 1 | 星期二 | FALSE | FALSE |
为什么不用日期类型做主键?
- ❌ JOIN 性能差(日期比较慢于整数)
- ❌ 不便于处理未知日期
- ❌ 不同系统日期格式可能不同
特殊日期处理:
-- 未知日期
INSERT INTO dim_date VALUES (0, NULL, '未知', ...);
-- 尚未发生
INSERT INTO dim_date VALUES (99991231, '9999-12-31', '未来', ...);
第五章:高级维度技术
5.1 维度外展 (Dimension Outriggers)
定义:维度表引用另一个维度表(不推荐,但有时必要)。
示例:
-- 主维度表
CREATE TABLE dim_product (
product_key INT PRIMARY KEY,
product_name VARCHAR(100),
brand_key INT, -- 外展到品牌维度
...
);
-- 外展维度表
CREATE TABLE dim_brand (
brand_key INT PRIMARY KEY,
brand_name VARCHAR(50),
brand_manager VARCHAR(50),
brand_category VARCHAR(50)
);
何时使用:
- 品牌信息需要单独维护
- 品牌信息更新频繁
- 多个维度共享品牌信息
⚠️ 注意:会增加查询复杂度,谨慎使用
5.2 桥接表 (Bridge Tables)
定义:处理多对多关系的辅助表。
场景 1:多值属性
问题:一个客户有多个电话号码
-- ❌ 错误做法:用分隔符
customer_phones VARCHAR(200) -- '1234,5678,9012'
-- ✅ 正确做法:桥接表
CREATE TABLE bridge_customer_phone (
customer_key INT,
phone_number VARCHAR(20),
phone_type VARCHAR(20),
PRIMARY KEY (customer_key, phone_number)
);
场景 2:分组维度
问题:分析账户组的销售
-- 账户维度
CREATE TABLE dim_account (
account_key INT PRIMARY KEY,
account_name VARCHAR(100)
);
-- 账户组维度
CREATE TABLE dim_account_group (
group_key INT PRIMARY KEY,
group_name VARCHAR(50)
);
-- 桥接表:一个账户可以属于多个组
CREATE TABLE bridge_account_group (
account_key INT,
group_key INT,
allocation_percentage DECIMAL(5,2), -- 分配比例
PRIMARY KEY (account_key, group_key)
);
使用示例:
-- 查询某个账户组的销售额
SELECT g.group_name, SUM(f.sales_amount * b.allocation_percentage / 100)
FROM fact_sales f
JOIN bridge_account_group b ON f.account_key = b.account_key
JOIN dim_account_group g ON b.group_key = g.group_key
WHERE g.group_name = '重点客户组'
GROUP BY g.group_name;
5.3 多货币处理
场景:跨国企业需要处理多种货币
方案 1:在事实表中存储多种货币
CREATE TABLE fact_sales (
date_key INT,
product_key INT,
-- 本地货币
local_currency_code VARCHAR(3),
local_sales_amount DECIMAL(12,2),
-- 报告货币(如美元)
reporting_currency_code VARCHAR(3),
reporting_sales_amount DECIMAL(12,2),
-- 汇率
exchange_rate DECIMAL(10,6)
);
方案 2:货币维度
CREATE TABLE dim_currency (
currency_key INT PRIMARY KEY,
currency_code VARCHAR(3),
currency_name VARCHAR(50)
);
CREATE TABLE fact_exchange_rate (
date_key INT,
from_currency_key INT,
to_currency_key INT,
exchange_rate DECIMAL(10,6),
PRIMARY KEY (date_key, from_currency_key, to_currency_key)
);
5.4 多时区处理
挑战:跨时区的业务需要统一时间
解决方案:
CREATE TABLE fact_web_events (
event_key BIGINT PRIMARY KEY,
-- UTC 时间(标准)
event_timestamp_utc TIMESTAMP,
utc_date_key INT,
utc_time_key INT,
-- 本地时间
event_timestamp_local TIMESTAMP,
local_date_key INT,
local_time_key INT,
-- 时区
timezone_key INT,
...
);
CREATE TABLE dim_timezone (
timezone_key INT PRIMARY KEY,
timezone_name VARCHAR(50),
utc_offset INT
);
第六章:特殊场景的维度建模
6.1 实时/准实时分区
挑战:部分数据需要实时更新,部分数据是历史稳定数据
解决方案:分区策略
-- 历史分区(只读)
CREATE TABLE fact_sales_history (
date_key INT,
product_key INT,
...
) PARTITION BY RANGE (date_key);
-- 当前分区(可更新)
CREATE TABLE fact_sales_current (
date_key INT,
product_key INT,
...
);
-- 视图合并
CREATE VIEW fact_sales AS
SELECT * FROM fact_sales_history
UNION ALL
SELECT * FROM fact_sales_current;
6.2 大维度表处理
问题:客户维度有数亿行怎么办?
解决方案:
1. 微型维度 (Mini-Dimension) 将频繁变化的属性分离出来
-- 客户基本维度(相对稳定)
CREATE TABLE dim_customer_base (
customer_key INT PRIMARY KEY,
customer_id VARCHAR(20),
customer_name VARCHAR(100),
birth_date DATE,
gender VARCHAR(10)
);
-- 客户人口统计微型维度(频繁变化)
CREATE TABLE dim_customer_demographics (
demographics_key INT PRIMARY KEY,
age_range VARCHAR(20),
income_level VARCHAR(20),
credit_score_range VARCHAR(20)
);
-- 事实表同时引用两个维度
CREATE TABLE fact_sales (
customer_key INT,
demographics_key INT,
...
);
2. 分片维度 (Shrunken Dimension) 创建简化版本的维度
-- 完整产品维度(数百万行)
CREATE TABLE dim_product_full (
product_key INT PRIMARY KEY,
product_name VARCHAR(100),
brand VARCHAR(50),
category VARCHAR(50),
... -- 100个属性
);
-- 简化产品维度(只保留常用属性)
CREATE TABLE dim_product_shrunken (
product_key INT PRIMARY KEY,
product_name VARCHAR(100),
brand VARCHAR(50),
category VARCHAR(50)
-- 只有最常用的10个属性
);
6.3 稀疏维度处理
问题:某些维度组合很少出现,导致大量空值
解决方案:条件维度
-- 不好的设计
CREATE TABLE fact_sales (
date_key INT,
product_key INT,
promotion_key INT, -- 大部分时间为空
...
);
-- 改进:只在有促销时才关联促销维度
CREATE TABLE fact_sales (
date_key INT,
product_key INT,
promotion_key INT DEFAULT 0, -- 0表示无促销
...
);
-- 促销维度包含"无促销"记录
INSERT INTO dim_promotion VALUES (0, '无促销', ...);
6.4 层次结构的处理
问题:组织架构等层次结构可能很深
解决方案 1:扁平化(推荐)
CREATE TABLE dim_organization (
org_key INT PRIMARY KEY,
-- 各层级都存储在一行中
employee_name VARCHAR(100),
level1_manager VARCHAR(100),
level2_manager VARCHAR(100),
level3_manager VARCHAR(100),
level4_manager VARCHAR(100),
department VARCHAR(50),
division VARCHAR(50),
company VARCHAR(50)
);
解决方案 2:路径枚举
CREATE TABLE dim_organization_hierarchy (
org_key INT PRIMARY KEY,
employee_name VARCHAR(100),
full_path VARCHAR(500), -- 如:'/公司/事业部/部门/小组/员工'
level INT,
parent_key INT
);
附录:维度建模最佳实践总结
✅ 设计原则
- 从业务出发 - 理解业务流程,不是技术优先
- 保持简单 - 星型模型优于雪花模型
- 原子粒度 - 从最细粒度开始设计
- 一致性 - 一致性维度和事实贯穿整个企业
- 用户友好 - 使用业务术语,易于理解
✅ 事实表设计检查清单
- 粒度是否明确?
- 是否选择了正确的事实表类型?(事务/周期快照/累积快照)
- 度量是否与粒度一致?
- 是否包含所有相关维度外键?
- 退化维度是否合理?
- 事实是否可加/半可加/不可加?
✅ 维度表设计检查清单
- 是否使用代理键作为主键?
- 是否包含足够的描述性属性?
- 是否考虑了缓慢变化维度策略?
- 层次结构是否扁平化存储?
- 是否避免了不必要的雪花化?
- 是否有未知/不适用的默认行?
✅ 常见错误
| 错误 | 正确做法 |
|---|---|
| ❌ 在事实表中存储文本描述 | ✅ 移到维度表 |
| ❌ 过度规范化(雪花化) | ✅ 使用星型模型 |
| ❌ 使用业务键做主键 | ✅ 使用代理键 |
| ❌ 不处理缓慢变化维度 | ✅ 选择合适的SCD类型 |
| ❌ 粒度不明确 | ✅ 明确声明粒度 |
| ❌ 事实和维度混淆 | ✅ 区分度量和属性 |
📊 事实表类型选择指南
决策树:
业务场景是什么?
├── 记录每次交易/事件
│ └── → 事务事实表
├── 需要定期快照(库存、余额)
│ └── → 周期快照事实表
├── 跟踪业务流程(订单、理赔)
│ └── → 累积快照事实表
└── 只记录事件发生(无度量)
└── → 无事实事实表
📊 SCD 类型选择指南
决策树:
属性会变化吗?
├── 否
│ └── → SCD Type 0
└── 是
├── 需要保留完整历史吗?
│ ├── 是 → SCD Type 2 (最常用)
│ └── 否
│ ├── 只需要前后对比?
│ │ └── → SCD Type 3
│ └── 不需要历史
│ └── → SCD Type 1
└── 历史查询很少?
└── → SCD Type 4
🎯 设计模式速查
| 场景 | 推荐方案 | 章节 |
|---|---|---|
| 基本维度建模 | 星型模型 | 1.3 |
| 记录每次交易 | 事务事实表 | 3.1 |
| 定期快照 | 周期快照事实表 | 3.2 |
| 业务流程跟踪 | 累积快照事实表 | 3.3 |
| 无数值度量 | 无事实事实表 | 3.4 |
| 保留完整历史 | SCD Type 2 | 4.5 |
| 一维多角色 | 角色扮演维度 | 4.6 |
| 多个标志字段 | 杂项维度 | 4.7 |
| 多对多关系 | 桥接表 | 5.2 |
| 大维度表 | 微型维度/分片维度 | 6.2 |
实战案例
案例 1:电商销售数据仓库
业务过程:在线销售
粒度:每个订单行项
维度:
- 日期维度(下单日期)
- 产品维度
- 客户维度
- 促销维度
- 支付方式维度
事实:
- 销售数量
- 销售金额
- 折扣金额
- 成本
- 利润
模型设计:
sql
-- 事实表
CREATE TABLE fact_online_sales (
date_key INT,
product_key INT,
customer_key INT,
promotion_key INT,
payment_method_key INT,
order_number VARCHAR(20), -- 退化维度
line_number INT, -- 退化维度
quantity DECIMAL(10,2),
sales_amount DECIMAL(12,2),
discount_amount DECIMAL(12,2),
cost_amount DECIMAL(12,2),
profit_amount DECIMAL(12,2),
PRIMARY KEY (order_number, line_number)
);
案例 2:银行账户快照
业务过程:账户状态跟踪
粒度:每天每个账户的状态
事实表类型:周期快照
模型设计:
sql
CREATE TABLE fact_account_snapshot (
date_key INT,
account_key INT,
customer_key INT,
branch_key INT,
-- 半可加事实
opening_balance DECIMAL(15,2),
closing_balance DECIMAL(15,2),
-- 可加事实
total_deposits DECIMAL(12,2),
total_withdrawals DECIMAL(12,2),
service_charges DECIMAL(10,2),
interest_earned DECIMAL(10,2),
-- 不可加事实
average_daily_balance DECIMAL(15,2),
PRIMARY KEY (date_key, account_key)
);
sql
CREATE TABLE dim_organization_hierarchy (
org_key INT PRIMARY KEY,
employee_name VARCHAR(100),
full_path VARCHAR(500), -- 如:'/公司/事业部/部门/小组/员工'
level INT,
parent_key INT
);
附录:维度建模最佳实践总结
✅ 设计原则
- 从业务出发 - 理解业务流程,不是技术优先
- 保持简单 - 星型模型优于雪花模型
- 原子粒度 - 从最细粒度开始设计
- 一致性 - 一致性维度和事实贯穿整个企业
- 用户友好 - 使用业务术语,易于理解
✅ 事实表设计检查清单
- 粒度是否明确?
- 是否选择了正确的事实表类型?(事务/周期快照/累积快照)
- 度量是否与粒度一致?
- 是否包含所有相关维度外键?
- 退化维度是否合理?
- 事实是否可加/半可加/不可加?
✅ 维度表设计检查清单
- 是否使用代理键作为主键?
- 是否包含足够的描述性属性?
- 是否考虑了缓慢变化维度策略?
- 层次结构是否扁平化存储?
- 是否避免了不必要的雪花化?
- 是否有未知/不适用的默认行?
✅ 常见错误
| 错误 | 正确做法 |
|---|---|
| ❌ 在事实表中存储文本描述 | ✅ 移到维度表 |
| ❌ 过度规范化(雪花化) | ✅ 使用星型模型 |
| ❌ 使用业务键做主键 | ✅ 使用代理键 |
| ❌ 不处理缓慢变化维度 | ✅ 选择合适的SCD类型 |
| ❌ 粒度不明确 | ✅ 明确声明粒度 |
| ❌ 事实和维度混淆 | ✅ 区分度量和属性 |
📊 事实表类型选择指南
决策树:
业务场景是什么?
├── 记录每次交易/事件
│ └── → 事务事实表
├── 需要定期快照(库存、余额)
│ └── → 周期快照事实表
├── 跟踪业务流程(订单、理赔)
│ └── → 累积快照事实表
└── 只记录事件发生(无度量)
└── → 无事实事实表
📊 SCD 类型选择指南
决策树:
属性会变化吗?
├── 否
│ └── → SCD Type 0
└── 是
├── 需要保留完整历史吗?
│ ├── 是 → SCD Type 2 (最常用)
│ └── 否
│ ├── 只需要前后对比?
│ │ └── → SCD Type 3
│ └── 不需要历史
│ └── → SCD Type 1
└── 历史查询很少?
└── → SCD Type 4
🎯 设计模式速查
| 场景 | 推荐方案 | 章节 |
|---|---|---|
| 基本维度建模 | 星型模型 | 1.3 |
| 记录每次交易 | 事务事实表 | 3.1 |
| 定期快照 | 周期快照事实表 | 3.2 |
| 业务流程跟踪 | 累积快照事实表 | 3.3 |
| 无数值度量 | 无事实事实表 | 3.4 |
| 保留完整历史 | SCD Type 2 | 4.5 |
| 一维多角色 | 角色扮演维度 | 4.6 |
| 多个标志字段 | 杂项维度 | 4.7 |
| 多对多关系 | 桥接表 | 5.2 |
| 大维度表 | 微型维度/分片维度 | 6.2 |
更多推荐
所有评论(0)