你是幸福社区的银行客户经理老陈,手上有一摞客户档案卡。客户老张从幸福小区搬到了阳光花园——你会怎么改他的档案卡? 直接涂改?最新地址有了,可三年前“幸福小区”那段历史就蒸发了,总部做“2021 年幸福小区客户渗透率”分析时再也找不到老张。保留旧卡再抄一张新卡?历史完整了,可柜员一翻档案发现两张卡都写着老张,分不清哪张是“现在”。在档案卡上多印一栏“上址”?能对比上一回,但也只能回忆到上一回。 这个不起眼的日常难题,正是数据仓库建模里大名鼎鼎的“缓慢变化维”(Slowly Changing Dimension,SCD)要解决的核心问题。数据仓库里,客户、产品、门店这类“维度”会随时间慢慢变脸,而事实表(每天的订单)却对应着一个个具体的历史时刻。让“当时的订单”准确对得上“当时的样子”,是一门艺术。 本文就用老陈的档案卡串起 SCD 的三种套路:Type 1 涂改、Type 2 贴新页、Type 3 加一栏,并讲清代理键、有效期字段和如何选型。

岗位关联

理解缓慢变化维是数据仓库建模的分水岭:掌握它,你就能在报表口径漂移、历史追溯失效等真实项目中给出靠谱方案,这也是从事数据分析、特征工程与数据治理的必备基本功。

目录
  1. 可视化演示
  2. 维度变化本质
  3. SCD基本概念
  4. Type1覆盖
  5. Type2版本化
  6. 代理键设计
  7. Type3增列
  8. SCD类型选择
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

维度变化本质

老陈的档案卡上写满了客户的“小档案”:住址、手机号、职业、会员等级。这些字段有个共同特点——会变。老张搬家、换工作、升金卡,每次只改一个字段,看似平常。可一旦卡片上的字变了,过去的事实就容易被“偷偷改写”。

在数据仓库里,客户、产品、门店这类“分析角度”叫维度(Dimension),角度上的具体描述叫维度属性(Attribute);订单、交易这类持续发生的“度量”叫事实(Fact)。事实表里每一行都对应一个具体的历史时刻,而维度属性却会随时间漂移。于是出现一道难题:2020 年的那笔订单,到底属于“幸福小区的老张”还是“阳光花园的老张”?

这就是“维度变化”的本质:同一业务主体的属性值,在不同时刻可以取不同的值。我们可以用一个简洁的数学表达来刻画它:

老陈终于意识到,档案卡不只回答“现在住哪”,更要回答“一路是怎么住过来的”。只有把“变化”当成研究对象,每一笔历史订单才能找到它真正的“当事人”。

f(k,t)=vf(k, t) = v
这个公式是说:给定一个人(业务键 k)和一个时间点 t,就能算出他那时的属性值 v。SCD 的全部工作,就是让这个"f"随时可查、且不乱改。
  1. 函数 f表示“维度状态随时间的映射关系”,输入时间和业务键,输出属性值。f(⋅,⋅)f(\cdot, \cdot)
  2. 业务键 k维度的稳定标识,如客户编号 G001,在时间里不改变。k=G001k = \text{G001}
  3. 时间点 t快照发生的时刻,比如“2021 年 5 月 1 日”。t=2021-05-01t = \text{2021-05-01}
  4. 属性值 v业务键在 t 时刻的具体取值,如地址“幸福小区”。v=幸福小区v = \text{幸福小区}

SCD基本概念

老陈翻出了前辈留下的《客户档案管理手册》。手册里写着:客户的住址、电话这类属性变化不频繁,但一旦变了就影响历史。这种“又慢又关键”的变化,在数据仓库里有个专门的名字——缓慢变化维(Slowly Changing Dimension,SCD)。注意,“缓慢”不是指变化速度慢,而是相对事实表每天百万级的新增,维度属性的变化是低频的。

手册同时警告:如果不处理维度变化,最直接的后果叫“口径漂移”。上个月的报表说老张住在幸福小区,这个月他搬了家,同一张报表就变成了阳光花园;事实表的外键、聚合的分组字段、训练模型的样本特征,全部跟着变。更麻烦的是,当你想把 2021 年 5 月的订单拉出来重算,系统会拿“现在的地址”去匹配,历史事实被系统性篡改。

为此,业界归纳出了三种标准化套路:Type 1、Type 2、Type 3。一次属性变化可以抽象成这样一个过程:

老陈把三种套路抄在便签上贴在工位——涂改、贴页、加栏——先记下来,再逐个推演。

vt0→vt1(t0<t1, vt0≠vt1)v_{t_0} \rightarrow v_{t_1} \quad (t_0 < t_1,\ v_{t_0} \neq v_{t_1})
这个式子描述的是一次“属性漂移”:从旧值 v_t0 变成新值 v_t1。三种 SCD 类型的差别,就在于如何处理这个箭头右侧的旧值。
  1. 旧值 v_t0变化发生前的属性取值,如老张 2020 年住在幸福小区。vt0=幸福小区v_{t_0} = \text{幸福小区}
  2. 新值 v_t1变化后的属性取值,如 2022 年搬到阳光花园。vt1=阳光花园v_{t_1} = \text{阳光花园}
  3. 条件约束强调 t0 早于 t1 且值确实不同,SCD 处理的就是这个“漂移”过程。t0<t1, vt0≠vt1t_0 < t_1, \ v_{t_0} \neq v_{t_1}

Type1覆盖

Type 1 就是老陈最自然的反应:拿起橡皮擦,把“幸福小区”擦掉,写上“阳光花园”。干净利落,一张卡片始终反映最新状态。

技术上就是一条 UPDATE 语句:按业务键定位到那一行,直接把属性改成新值,旧值从维表里彻底消失。用集合的视角来看,就是先从集合中删掉旧的一对(业务键,旧值),再并入新的一对:

Type 1 的优点:实现最简单、查询最快、不增加任何存储;缺点也致命:历史信息永久丢失,历史报表无法重算。适用场景包括:源数据纠错(比如地址本来就是录错的)、属性没有历史分析价值(如产品包装色号微调)、业务明确只关心“当前是什么”。

老陈用 Type 1 改完老张的卡片后,一个月后总部要“2021 年幸福小区客户渗透率”,名单里永远少了一个人。他默默把橡皮擦收进了抽屉。

D←(D∖{(k, vold)})∪{(k, vnew)}\displaystyle D \leftarrow (D \setminus \{(k,\ v_{\text{old}})\}) \cup \{(k,\ v_{\text{new}})\}
意思是:整张维表就像一大袋卡片,Type 1 先把写着旧地址的那张抽出来扔掉,再塞进一张写着新地址的新卡。最后袋子里永远只有一种版本。
  1. 集合 D把整张维度表看成一个“业务键 + 属性值”对的集合。D={(k,v)}D = \{ (k, v) \}
  2. 删除旧对先移除 (k, v_old),旧值从集合中消失,历史彻底蒸发。D∖{(k, vold)}D \setminus \{(k,\ v_{\text{old}})\}
  3. 并入新对再把 (k, v_new) 加进来,集合里只剩最新状态。∪ {(k, vnew)}\cup \ \{(k,\ v_{\text{new}})\}

Type2版本化

Type 2 是“贴新页”:旧卡片保留原样,在档案卡册里再贴一张新卡。两张都写着老张,但各自标注“这份是 2020-2022 年的他”“这份是 2022 年至今的他”。查历史翻旧卡,查现状看新卡。

技术上对应“新增版本行”:维表里同一个业务键可以有多行,每行代表一个版本。典型字段是:valid_from(版本生效日)、valid_to(版本失效日,当前版本为无限大)、is_current(是否当前版本)、version(第几版)。当新变化到达时,先关闭旧版本(valid_to = 今天,is_current = 0),再插入新版本(valid_from = 今天,valid_to = 无限大,is_current = 1)。

查询时,给定时间点 t,只要筛选出满足“有效期内”的那一行,就能还原 t 时刻的真实状态:

代价是:行数随变化次数增长,ETL 从“更新一行”变成“更新加插入”两个动作,事实表关联时也必须明确“用哪个版本”。但换来的是完整、不可篡改的历史,是监管审计和时间穿透分析的最爱。

老陈的档案卡册从一张卡变成三张卡。柜员查“现在住哪”只看 is_current 标记,总部查“2021 年老张住哪”翻到对应页——再没人来找他扯皮。

v(t)=vi,s.t. valid_fromi≤t<valid_toiv(t) = v_i, \quad \text{s.t. } \text{valid\_from}_i \le t < \text{valid\_to}_i
意思是:任意给一个时间点,都能找到唯一对应的版本行。这就像查“老张 2021 年住哪”,翻到标注 2020-2022 的那页档案即可,答案稳定不变。
  1. 版本 i同一业务键会维护多个版本行,每个版本都有自己的有效期。i∈{1,2,…,n}i \in \{1,2,\dots,n\}
  2. 开始时间版本 i 生效的起始日期,如 2020-01-01。valid_fromi\text{valid\_from}_i
  3. 结束时间版本 i 失效的截止日期,当前版本的 valid_to 是无限大(NULL 或 9999-12-31)。valid_toi=INF\text{valid\_to}_i = \text{INF}
  4. 时间点判定给定 t 落入哪个版本的有效期,就取哪个版本的值——这就是“时间穿透”。valid_fromi≤t<valid_toi\text{valid\_from}_i \le t < \text{valid\_to}_i

代理键设计

档案卡册里出现了三张“老张”,问题来了:卡片编号怎么编?如果沿用老张的身份证号,三张卡编号一模一样,根本分不清哪一版。老陈决定给每张卡一个独立的“存档编号”——这就是代理键。

业务键(Business Key)是源系统的主键,语义稳定但会重复(多版本下);代理键(Surrogate Key)是数仓自行生成的整数主键,没有业务含义,唯一标识一行版本。事实表的外键不再指向业务键,而是指向代理键——“哪笔订单对应哪一版档案”被精确锁定。

设计要点:代理键必须稳定,绝不随属性变化而改变;Type 2 下每新增一个版本就生成一个新代理键;一般用自增整数或序列实现。老陈在每张卡左上角贴了编号 1001、1002、1003——以后不管核对历史还是查现状,报出编号就能抽出那一版档案。

BK↦{SK1,SK2,…,SKn},FK=SKj\text{BK} \mapsto \{ \text{SK}_1, \text{SK}_2, \ldots, \text{SK}_n \}, \quad \text{FK} = \text{SK}_j
意思是:一个业务键(G001)可以对应多个代理键(1001、1002、1003),事实表的外键只要等于其中某个代理键,就唯一指定了那一版。
  1. 业务键 BK源系统主键,如客户编号 G001,在 Type 2 下会重复出现。BK=G001\text{BK} = \text{G001}
  2. 代理键集合一个业务键映射到 n 个代理键,对应 n 个版本行。BK↦{SK1,SK2,…,SKn}\text{BK} \mapsto \{\text{SK}_1, \text{SK}_2, \dots, \text{SK}_n\}
  3. 事实表外键事实表通过某一个代理键,精确指向某个版本。FK=SKj\text{FK} = \text{SK}_j

Type3增列

老陈的第三种备选方案:不贴新卡,也不涂改,而是在卡片上加印一栏“上址”。搬家时,把原来的地址挪进“上址”栏,新地址写进“住址”栏。一张卡走天下。

技术上对应“新增列”:维表增加一列 attr_prev(上期值),更新逻辑分两步——先把当前值复制给上期列,再把新值写进当前列:

Type 3 的优点是查询简单、改一张卡就行,特别适合“本期 vs 上期”的对比分析,例如“客户改地址前后的消费差异”。局限也很明显:没有时间字段,无法回答“2021 年 5 月住哪”;要追溯 N 层就得加 N 列,几乎不可扩展。

老张再搬一次家后,老陈的上址栏只写着“阳光花园”,最初的“幸福小区”早被挤出了卡片。老陈叹气:这个方案只适合“记性不用太好”的场合。

attrprev←attrcur,attrcur←vnew\text{attr}_{\text{prev}} \leftarrow \text{attr}_{\text{cur}}, \quad \text{attr}_{\text{cur}} \leftarrow v_{\text{new}}
意思是:老地址“退位让贤”到上期栏,新地址“登基”到当前栏。行数不变,只是列里的字挪了个位置。
  1. 旧值让位把当前值先复制到“上期”列,保证旧信息不丢。attrprev←attrcur\text{attr}_{\text{prev}} \leftarrow \text{attr}_{\text{cur}}
  2. 新值上位再把新值写入当前列,完成一次更新。attrcur←vnew\text{attr}_{\text{cur}} \leftarrow v_{\text{new}}
  3. 行数不变整个过程只改一行、不改行数,也不记录变化时间。row_count 不变\text{row\_count} \text{ 不变}

SCD类型选择

三套方案摆在桌上:橡皮擦、贴新页、加一栏,选哪套?老陈的决策逻辑后来写进了手册:先问三个问题——要不要追溯历史?要追溯到多深?能承受多大的存储与维护成本?

对比一下:Type 1 成本最低但没有历史;Type 2 成本最高但历史最全;Type 3 处于中间,只支持一层对比。对报表口径的影响更关键:Type 1 会让历史报表口径随时漂移;Type 2 能保证任何历史时点重算结果一致(时间穿透);Type 3 只能支撑“上期对比”型报表。如果监管要求客户信息可追溯,Type 1 会直接让审计失败。

决策可以抽象成一个加权打分问题:

还有一个进阶技巧:同一张维表的不同属性可以“混合策略”。例如客户地址用 Type 2(值得追溯),手机号用 Type 1(没人关心历史)。很多数仓从 Type 1 起步,随着监管和业务分析升级,逐步演进到 Type 3、Type 2。

老陈最终在手册里写下:地址、职业、等级这类“有故事”的字段用 Type 2;纠错类更新用 Type 1;只需要对比上期时用 Type 3。档案卡册从此井井有条。

type∗=arg⁡max⁡c∈{1,2,3}(α⋅valuec−β⋅costc)\displaystyle \text{type}^* = \arg\max_{c \in \{1,2,3\}} \left( \alpha \cdot \text{value}_c - \beta \cdot \text{cost}_c \right)
意思是:把每种策略的“历史价值”乘以权重 α,减去“实现成本”乘以权重 β,得分最高的就是你的选择。α 越大越看重历史,β 越大越看重省钱省事。
  1. 候选集合三种策略 Type 1/2/3 作为待选方案。c∈{1,2,3}c \in \{1, 2, 3\}
  2. 历史价值 value_c策略能支撑的时间回溯能力,Type 2 最高,Type 1 最低。valuec\text{value}_c
  3. 实现成本 cost_c存储膨胀与 ETL 复杂度,Type 2 最高,Type 1 最低。costc\text{cost}_c
  4. 加权取舍用权重 α、β 折算总收益,取最大者作为最终选型。arg⁡max⁡c(α⋅valuec−β⋅costc)\arg\max_c \left( \alpha \cdot \text{value}_c - \beta \cdot \text{cost}_c \right)

AI 应用场景

用户流失预警的特征回溯

阶段:特征工程与样本对齐

解决的问题:传统用户宽表只保留最新字段,训练模型时把“2021 年的行为”和“2024 年的会员等级”错配在一起,产生特征穿越(data leakage),模型看起来准确、上线却失效。Type 2 让“特征按业务发生日取值”成为可能。

具体案例:某电商平台对用户等级维度实施 Type 2 版本化(代理键 + valid_from/valid_to),流失预警模型训练时按订单日期 join 当时的等级版本,样本质量大幅改善,离线 AUC 提升约 0.06,上线后召回率提升 8%。

银行客户信息合规溯源

阶段:数据治理与模型审计

解决的问题:金融监管要求保留客户地址变更历史,传统 Type 1 覆盖后无法回答“2020 年填报的地址是什么”,KYC 审计直接不通过。Type 2 + 代理键让“任意时点的客户档案”都能秒级还原。

具体案例:某城商行对客户地址、职业字段实施 Type 2 版本化,反洗钱审计查询从“人工翻纸质回执三天”变成“一条 SQL 十分钟”,同时支撑了可疑交易模型的回溯验证。

组织架构调整与业绩归因

阶段:数据分析与决策支持

解决的问题:员工从华东区调往华南区,销售业绩需要按“当日所属区域”归因。若维表用 Type 1 覆盖,历史业绩会全部被归到新区域,奖金核算与经营分析产生纠纷。

具体案例:某零售企业组织维度采用 Type 2,月度经营分析可以还原“当时的大区”口径,管理层的时间穿透报表与财务核算系统完全对齐,避免了跨区调动的历史业绩争议。

Python 演示

python58 行
# -*- coding: utf-8 -*-"""缓慢变化维 Type 2 增量装载演示: 客户地址变更时新增版本行而非覆盖"""import pandas as pdfrom datetime import date INF = date(9999, 12, 31) # 模拟历史维表: 客户 G001 已有一个当前版本dim = pd.DataFrame([    {"customer_id": "G001", "address": "幸福小区",     "valid_from": date(2020, 1, 1), "valid_to": INF,     "is_current": 1, "version": 1}]) # 模拟今日源系统数据: G001 搬家, G002 是新客户source = pd.DataFrame([    {"customer_id": "G001", "address": "阳光花园"},    {"customer_id": "G002", "address": "梧桐里"}])TODAY = date(2022, 6, 1)  def scd2_load(dim, source, today):    for _, row in source.iterrows():        cid = row["customer_id"]        latest = dim[(dim["customer_id"] == cid) & (dim["is_current"] == 1)]        if latest.empty:            new_row = pd.DataFrame([{                "customer_id": cid, "address": row["address"],                "valid_from": today, "valid_to": INF,                "is_current": 1, "version": 1            }])            dim = pd.concat([dim, new_row], ignore_index=True)            continue        if latest.iloc[0]["address"] == row["address"]:            continue        idx = latest.index[0]        dim.loc[idx, "valid_to"] = today        dim.loc[idx, "is_current"] = 0        new_row = pd.DataFrame([{            "customer_id": cid, "address": row["address"],            "valid_from": today, "valid_to": INF,            "is_current": 1, "version": int(latest.iloc[0]["version"]) + 1        }])        dim = pd.concat([dim, new_row], ignore_index=True)    return dim  def query_at(dim, cid, t):    hit = dim[(dim["customer_id"] == cid) &              (dim["valid_from"] <= t) & (dim["valid_to"] > t)]    return hit.iloc[0]["address"] if not hit.empty else None  dim = scd2_load(dim, source, TODAY)print(dim.to_string(index=False))print("2021年G001地址:", query_at(dim, "G001", date(2021, 5, 1)))print("2026年G001地址:", query_at(dim, "G001", date(2026, 5, 1)))
目的:模拟 Type 2 的增量装载——客户搬家时关闭旧版本、开启新版本,并支持任意时间点的地址回溯查询。
  1. latest = dim[(dim["customer_id"] == cid) & (dim["is_current"] == 1)]定位该客户当前有效的那一版档案。
  2. if latest.empty:源系统出现新客户时,直接插入第一版,版本号从 1 开始。
  3. if latest.iloc[0]["address"] == row["address"]: continue地址没变就跳过,避免生成无意义的版本。
  4. dim.loc[idx, "valid_to"] = today关闭旧版本:写入失效日期,它从此成为历史。
  5. dim.loc[idx, "is_current"] = 0同时摘掉旧版本的“当前”标识。
  6. "version": int(latest.iloc[0]["version"]) + 1新版本号在老版本号基础上递增,形成版本链。
  7. dim = pd.concat([dim, new_row], ignore_index=True)追加新版本行,实现“贴新页”。
  8. hit = dim[(dim["customer_id"] == cid) & (dim["valid_from"] <= t) & (dim["valid_to"] > t)]时间点查询:用有效期条件筛出 t 时刻唯一有效的版本。

常见误区

误区“我只要在维表里加一个 create_time 或 update_time 字段,就能追溯历史了吧?”

正解时间戳只能告诉你“这行什么时候被写入”,无法回答“某时刻哪个版本有效”。Type 2 需要一对有效期字段(valid_from / valid_to)外加当前标识,才能支撑时间点查询。就像档案卡只记录“上次改写时间”,并不能还原三年前的住址。

为何常见:大家习惯了数据库表的“最后更新时间”运维思维,把“记录变更”等同于“可回溯”。

误区“客户编号是唯一的,直接把 customer_id 当维表主键不就行了?”

正解Type 2 下同一个客户有多行版本,业务键必然重复,无法作为主键。必须引入代理键,事实表外键也指向代理键。业务键只能定位“是哪个人”,代理键才能定位“是哪一版”——正如身份证号只能找到人,找不到他某年住哪。

为何常见:源系统里业务键唯一的惯性思维,被直接带进了多版本的维表。

误区“每变一次就加一行,维表会无限膨胀,千万别用 Type 2。”

正解维度表行数 ≈ 业务实体数 × 平均变化次数,绝大多数维度(客户、产品)行数在百万量级,相比几十亿行的事实表几乎可以忽略。真正该关心的是事实表按代理键 join 的分区设计。监管、时序分析等硬需求不能用存储借口回避。

为何常见:拿事实表的体量吓自己,没有先估算维度表的真实规模。

误区“地址被误写成阳光花园,后来改回幸福小区,直接把历史行 UPDATE 回去不就行了?”

正解Type 2 的铁律是“版本行一旦写入不可修改”。更正也应该作为一次新版本写入:关闭当前版本、开启新版本,保留一条“变更时间线”。否则 2021 年的历史行被改动,历史报表会跟着变,产生“回溯性失真”。

为何常见:习惯了“上游改了我就覆盖”的源系统思维,没意识到维表是审计日志而不是最新状态。

误区“Type 3 加了历史列,不是也能查过去的值吗?”

正解Type 3 只有一列“上期值”,每次变化只保留最近一次“上一版”,既无法回答“三年前住哪”,也没有时间维度。它解决的是“现在 vs 上一回”的对比问题,不是历史追溯问题。

为何常见:看到“历史列”就以为支持完整历史,忽略了它只有一层且没有时间。

面试题

Q1. SCD 中 Type 1 的核心处理方式是什么?

单选题 · easy

  • A.新增一行来记录新值
  • B.新增一列来保存旧值
  • C.同时保留新旧两行数据
  • D.直接覆盖旧值不保留历史← 正确答案

答案:D。Type 1 就是直接 UPDATE 覆盖旧值,只保留最新状态。新增行是 Type 2,新增列是 Type 3,同时保留新旧两行也是 Type 2 的特征。就像用橡皮擦,永远只留下最新字迹。

Q2. 判断 Type 2 中“当前版本”行的典型字段特征是?

单选题 · easy

  • A.valid_to 为空且 is_current 标记为 1← 正确答案
  • B.valid_from 取最大值且 version 最小
  • C.只取 valid_from 最新的那一条
  • D.address 非空且状态正常的一条

答案:A。当前版本行必须满足 valid_to 为空(或 9999-12-31)且 is_current=1。B 选项自相矛盾(最新开始日期不会配最小版本号);C 只取最新开始日期可能选到已被关闭的历史版本;D 的过滤条件与版本状态无关。

Q3. 事实表关联维表时采用代理键而非业务键,主要原因是什么?

单选题 · mid

  • A.代理键用整数存储更节省空间
  • B.业务键在多版本下并不唯一← 正确答案
  • C.代理键包含了业务含义
  • D.代理键由源系统生成更可靠

答案:B。代理键存在的根本原因是业务键在 Type 2 下不唯一,事实表无法用业务键精确锁定版本。用整数只是实现细节,代理键恰恰不包含业务含义,也不是源系统生成的。

Q4. 相比 Type 2,Type 3 在历史保留上的局限性是什么?

单选题 · mid

  • A.无需新增任何列即可保留历史
  • B.只能保留最近一次的历史值← 正确答案
  • C.查询时必须关联多行版本
  • D.会导致事实表存储大幅膨胀

答案:B。Type 3 只保存最近一次“上期值”,无法往前追溯。A 说无需加列显然错误(Type 3 恰恰要加列);C 描述的是 Type 2 的多行查询;D 是 Type 2 的存储代价。就像“上址”栏永远只能写一条。

Q5. 以下哪些场景更适合采用 Type 2 策略?

多选题 · mid

  • A.需要按历史日期还原维度状态← 正确答案
  • B.监管审计要求信息可追溯← 正确答案
  • C.只关心客户当前最新状态
  • D.存储成本极其敏感的场景

答案:AB。按历史日期还原状态(如“2021 年老张住哪”)和监管审计追溯都是 Type 2 的典型应用。只关心最新状态应选 Type 1;存储成本敏感恰恰要避免 Type 2,因为版本行越多存储越大。

Q6. 客户地址被误改成“阳光花园”,后来更正回“幸福小区”,Type 2 的正确做法是什么?

单选题 · extreme

  • A.直接修改原版本行的地址为幸福小区
  • B.删除全部历史版本并重新建立一行
  • C.忽略该次更正不做任何处理操作
  • D.关闭当前版本再新增版本写幸福小区← 正确答案

答案:D。Type 2 的铁律是版本行一旦写入不可修改。即使源数据更正,也应关闭当前版本并新增一行写入正确值,保留完整变更时间线;直接 UPDATE 会破坏历史一致性,删除历史更不可取,忽略则数据错误。纠正历史要用“新版本”而不是“改旧账”。

Q7. 关于 SCD 类型的选择与比较,正确的说法有哪些?

多选题 · hard

  • A.同一张维表的不同属性可混合使用策略← 正确答案
  • B.Type 1 会破坏历史报表的口径一致性← 正确答案
  • C.Type 3 适合无限追溯完整历史场景
  • D.Type 2 可完整还原任意时刻维度状态← 正确答案

答案:ABD。同一张维表可对不同属性采用不同策略;Type 1 覆盖让历史报表口径随当前值漂移;Type 2 用有效期字段可还原任意时刻状态。Type 3 只保留一层历史,谈不上“无限追溯”,所以 C 错误。

Q8. 代理键设计应遵循的原则是什么?

单选题 · hard

  • A.自增整数且对外不暴露业务含义← 正确答案
  • B.沿用源系统主键保持业务含义
  • C.每次属性变化都重新生成键值
  • D.用字符串拼接所有维度属性作为键

答案:A。代理键应为自增整数且不暴露业务含义,与源系统解耦、长期稳定。沿用源主键会在多版本下冲突;每次属性变化重新生成会让外键关系错乱;拼接属性作为键则属性一变键就变,违背稳定原则。

Q9. “缓慢变化维”中的“缓慢变化”主要指什么?

单选题 · easy

  • A.维度属性值变化的频率相对较低← 正确答案
  • B.数据仓库表的查询响应速度慢
  • C.事实表数据的增长速度很缓慢
  • D.ETL 作业的整体运行时间较长

答案:A。“缓慢”是相对事实表高频增长而言,指维度属性值的变更频率低,而不是查询慢、增长慢或 ETL 慢。缓慢变化维处理的是“低频但有影响”的属性漂移。

Q10. 实施 Type 2 版本化时,哪些做法是正确的?

多选题 · hard

  • A.设置 valid_from 与 valid_to 记录版本有效期← 正确答案
  • B.新版本开启时同步关闭旧版本有效期← 正确答案
  • C.使用当前标识字段标记当前有效版本← 正确答案
  • D.为节省存储定期物理删除旧历史行

答案:ABC。Type 2 必须用 valid_from/valid_to 记录版本有效期,新版本开启时同步关闭旧版本(写入结束日期、清掉当前标识),并用 is_current 标记当前版。历史版本是用于时间穿透的资产,定期物理删除会破坏回溯能力,所以 D 错误。