数据仓库笔记 第三篇:常用缓慢变化维处理方式介绍

数据仓库笔记 第三篇:常用缓慢变化维处理方式介绍

摘要

数据仓库可以帮助我们追溯数据的历史变化,其中最主要的一个方法就是缓慢变化维。

此篇主要介绍最常用的缓慢变化维:Type1, Type2, Type3。

此笔记使用的数据库为SQLServer,相应的示例脚本都围绕于此,其它数据库的相应实现会略有不同。


什么是缓慢变化维?

在数据仓库中,维度表存储的是业务的描述性信息(如客户、产品、地区等)。这些属性会随着时间缓慢发生变化,如何处理这些变化,就是**缓慢变化维(SCD)**问题。

核心问题:当维度属性发生变化时,是覆盖旧值、保留历史,还是部分保留?


SCD 类型详解

📌 Type 1 - 直接覆盖(不保留历史)

原理:新值直接覆盖旧值,不保留任何历史记录。

最简单,粗暴的方法就是TRUNCATE + INSERT。

复制代码
┌─────────────────────────────────────────────────────────────┐
│                    Type 1 更新流程                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   【更新前】                                                 │
│   ┌──────────────────────────────────┐                     │
│   │ customer_id = 1001               │                     │
│   │ city = "北京"                     │                     │
│   └──────────────────────────────────┘                     │
│                    ↓                                        │
│              收到更新请求                                     │
│         city = "北京" → "上海"                               │
│                    ↓                                        │
│   【更新后】                                                 │
│   ┌──────────────────────────────────┐                     │
│   │ customer_id = 1001               │                     │
│   │ city = "上海"  ← 直接覆盖          │                     │
│   └──────────────────────────────────┘                     │
│                                                             │
│   ❌ 旧值"北京"永久丢失                                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

特点:

  • ✅ 实现简单,数据量小
  • ✅ 查询性能好(无历史版本)
  • ❌ 无法追溯历史状态
  • ❌ 历史报表可能不一致

适用场景:

  • 修正错误数据(拼写错误、格式问题)
  • 用户频繁更新的非关键字段(如昵称、头像)
  • 不需要历史追溯的属性(如备注信息)

📌 Type 2 - 新增历史记录(完整保留历史)

原理:为每次变化创建新记录,通过生效时间标识历史版本。

复制代码
┌─────────────────────────────────────────────────────────────┐
│                    Type 2 更新流程                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   【更新前】只有1条记录                                        │
│   ┌────────────────────────────────────────────┐           │
│   │ customer_id = 1001                         │           │
│   │ city = "北京"                               │           │
│   │ effective_start = 2025-01-01               │           │
│   │ effective_end = 9999-12-31  ← "永远有效"    │           │
│   │ is_current = 1              ← "当前版本"    │           │
│   └────────────────────────────────────────────┘           │
│                    ↓                                        │
│              收到更新请求                                     │
│         city = "北京" → "上海"                               │
│                    ↓                                        │
│   ┌─────────────────────────────────────────────────────┐  │
│   │ Step 1: 关闭旧记录                                    │  │
│   │ ┌────────────────────────────────────────────┐      │  │
│   │ │ customer_id = 1001                         │      │  │
│   │ │ city = "北京"                               │      │  │
│   │ │ effective_start = 2025-01-01               │      │  │
│   │ │ effective_end = 2026-04-26  ← 设置为当前日期 │      │  │
│   │ │ is_current = 0              ← 标记为历史版本 │      │  │
│   │ └────────────────────────────────────────────┘      │  │
│   └─────────────────────────────────────────────────────┘  │
│                    ↓                                        │
│   ┌─────────────────────────────────────────────────────┐  │
│   │ Step 2: 插入新记录                                    │  │
│   │ ┌────────────────────────────────────────────┐      │  │
│   │ │ customer_id = 1001                         │      │  │
│   │ │ city = "上海"  ← 新值                       │      │  │
│   │ │ effective_start = 2026-04-26  ← 当前日期    │      │  │
│   │ │ effective_end = 9999-12-31    ← "永远有效"  │      │  │
│   │ │ is_current = 1                ← "当前版本"  │      │  │
│   │ └────────────────────────────────────────────┘      │  │
│   └─────────────────────────────────────────────────────┘  │
│                                                             │
│   【更新后】共有2条记录                                       │
│   ✅ 完整保留了历史轨迹                                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

核心字段:

  • surrogate_key:代理键(自增主键)
  • effective_start_date / effective_end_date:生效时间区间,还有一种拼写方法是valid_from和valid_to。
  • is_current:是否当前版本标志

特点:

  • ✅ 完整保留历史轨迹
  • ✅ 支持时间点查询("某客户在某日期的地址是什么")
  • ✅ 历史报表准确可重现
  • ❌ 数据量增长快,但只是变化的数据才有增长,没变化的数据不会增长。所以增长基本都能控制在可接受的范围内。
  • ❌ ETL 逻辑略复杂
  • ❌ 查询需要额外过滤条件

适用场景:

  • 需要完整审计追踪(合规要求)
  • 历史数据分析(客户迁移路径、产品价格变化)
  • 财务、监管类场景

📌 Type 3 - 新增字段保留上一个版本

原理:增加"旧值"字段,只保留最近一次变化的前值。

复制代码
┌─────────────────────────────────────────────────────────────┐
│                    Type 3 更新流程                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   【更新前】                                                 │
│   ┌────────────────────────────────────────────┐           │
│   │ customer_id = 1001                         │           │
│   │ current_city = "北京"      ← 当前城市       │           │
│   │ previous_city = NULL       ← 上一个城市     │           │
│   │ city_change_date = NULL    ← 变更日期       │           │
│   └────────────────────────────────────────────┘           │
│                    ↓                                        │
│              收到更新请求                                     │
│         current_city = "北京" → "上海"                       │
│                    ↓                                        │
│   【更新中】字段值"移动"                                      │
│   ┌─────────────────────────────────────────────────────┐  │
│   │ previous_city ← current_city  (北京移入"上一值")      │  │
│   │ current_city ← "上海"         (新值成为"当前值")      │  │
│   │ city_change_date ← GETDATE()   (记录变更时间)         │  │
│   └─────────────────────────────────────────────────────┘  │
│                    ↓                                        │
│   【更新后】                                                 │
│   ┌────────────────────────────────────────────┐           │
│   │ customer_id = 1001                         │           │
│   │ current_city = "上海"      ← 当前城市       │           │
│   │ previous_city = "北京"     ← 上一个城市     │           │
│   │ city_change_date = 2026-04-26 ← 变更日期   │           │
│   └────────────────────────────────────────────┘           │
│                                                             │
│   ✅ 保留了上一个版本                                         │
│   ❌ 更早的"广州"历史已丢失(如果之前还有变更)                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

特点:

  • ✅ 数据量不膨胀
  • ✅ 可查看最近一次变化
  • ✅ 实现相对简单
  • ❌ 只能保留一个历史版本
  • ❌ 无法追溯更早的历史

适用场景:

  • 只需了解"上一个值是什么"
  • 分析"从什么变到什么"(客户从哪个城市迁来)
  • 需要对比变化但不需要完整历史

对比总结

对比维度 Type 1 Type 2 Type 3
历史保留 不保留 完整保留 保留上一个版本
数据量 稳定 持续增长 稳定
实现复杂度 简单 复杂 中等
查询复杂度 简单 需时间过滤 简单
审计追踪 无 完整 部分
适用场景 修正错误、非关键字段 合规审计、历史分析 对比分析

最佳实践建议

选择 SCD 类型的决策树

复制代码
┌─────────────────────────────────────────────────────────────┐
│                  SCD 类型选择决策树                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│                 维度字段发生变化                              │
│                        │                                    │
│                        ▼                                    │
│           ┌─────────────────────────┐                      │
│           │ 是否需要历史追踪?        │                      │
│           └─────────────────────────┘                      │
│                   ╱          ╲                              │
│                 否            是                            │
│                 │              │                            │
│                 ▼              ▼                            │
│         ┌────────────┐  ┌─────────────────────┐            │
│         │  Type 1    │  │ 需要完整历史还是      │            │
│         │  直接覆盖   │  │ 仅上一版本?          │            │
│         └────────────┘  └─────────────────────┘            │
│                              ╱          ╲                   │
│                         完整历史      仅上一版本              │
│                            │              │                 │
│                            ▼              ▼                 │
│                    ┌────────────┐  ┌────────────┐          │
│                    │  Type 2    │  │  Type 3    │          │
│                    │  新增记录   │  │  新增字段   │          │
│                    └────────────┘  └────────────┘          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Type 2 设计要点

复制代码
┌─────────────────────────────────────────────────────────────┐
│                  Type 2 设计核心要点                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌───────────────────────────────────────────────────┐    │
│   │ 1. 主键设计                                          │    │
│   │    ✅ 使用代理键(自增ID)作为主键                     │    │
│   │    ✅ 业务键(CustomerID)用于关联                    │    │
│   │    ❌ 不要用业务键做主键(会导致历史记录冲突)          │    │
│   └───────────────────────────────────────────────────┘    │
│                                                             │
│   ┌───────────────────────────────────────────────────┐    │
│   │ 2. 时间区间设计                                       │    │
│   │    ✅ 使用闭开区间 [start, end)                       │    │
│   │    ✅ 当前记录 end_date = '9999-12-31'                │    │
│   │    ✅ 查询条件: start <= 目标日期 AND end > 目标日期    │    │
│   └───────────────────────────────────────────────────┘    │
│                                                             │
│   ┌───────────────────────────────────────────────────┐    │
│   │ 3. 索引设计                                           │    │
│   │    ✅ (business_key, is_current)                     │    │
│   │    ✅ (business_key, start_date, end_date)          │    │
│   │    ✅ 过滤索引 WHERE is_current = 1                  │    │
│   └───────────────────────────────────────────────────┘    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

参考资料

  • Ralph Kimball - The Data Warehouse Toolkit
  • SQL Server 官方文档 - Temporal Tables
  • Microsoft Learn - Slowly Changing Dimensions

相关推荐
geovindu21 小时前
SQL Data Sampling and Approximation Techniques using postgreSQL 18
大数据·数据库·数据仓库·sql·postgresql·数据库开发·数据库架构
程序员梅雨2 天前
【从零开始学习数据仓库 | 第一篇】:维度建模理论
大数据·数据仓库·学习·阿里云·数据分析
棣廷2 天前
初识数据仓库——数仓分层到DataX数据采集
数据仓库
棣廷2 天前
初识Hive(2)
数据仓库·hive·hadoop
geovindu3 天前
Pivoting and Unpivoting Data in SQL using postgreSQL 18
大数据·数据库·数据仓库·sql·postgresql·数据库开发·数据库架构
数据库小学妹3 天前
OLAP数据库怎么选?看清OLTP、数仓、湖仓边界
数据仓库·mpp·列式存储·联机分析处理·数据库选型·olap数据库
程序 代码狂人3 天前
切片表与分区表的区别(数据开发面试版)
java·大数据·开发语言·数据仓库·算法
天河归来3 天前
从原始消息到业务数据:三层数据架构的设计与实践
数据库·数据仓库·架构
注塑机数据采集联网5 天前
中科晟达注塑机 MES 系统:从车间到数字化的智能制造实践
大数据·数据仓库·人工智能·物联网·制造·数据库架构