题目与适用范围
客户源每天发送一份包含 5000 万行的完整快照,约 1% 被选中保留历史的属性每天发生变化。订单持续到达,而源端修正最多会迟到 3 天。请设计一个缓慢变化维度 Type 2 客户维度,使每笔订单都能关联到业务发生时有效的客户属性,同时正确处理删除、安全重跑,并让回填后的历史仍可审计。
核心任务是维度建模和时间正确性。变更数据捕获可以提供输入事件,但不会替你定义目标历史。回答需要决定哪些属性值得使用 Type 2,区分持久业务键和版本代理键,定义互不重叠的有效区间,并说明事实表如何解析到正确版本。
规模会暴露草率设计。每天 1% 的变化率约等于每天新增 50 万个版本、每年新增 1.825 亿个版本。若每个新增版本平均占 300 字节,则每年约增加 54.75 GB 原始行数据,尚未计算索引、副本、元数据和压缩。这些数字是面试假设和容量估算起点,不是产品存储承诺。
面试官在考察什么
基础回答会说“关闭旧行,再插入新行”。高质量回答会先定义历史语义。Type 1 直接覆盖值,旧状态随之丢失;Type 2 为新状态创建带新代理键的版本,并保留旧版本。并非每个源字段都应触发新版本:修正大小写可以使用 Type 1,而用于历史报表的销售区域或定价分层通常适合 Type 2。
第二个信号是时间边界是否严谨。候选人应明确使用 [validfrom, validto) 这样的左闭右开区间,保证同一业务键的版本不重叠,并且只能有一个当前版本。时间 t 的事实应关联开始时间不晚于 t、结束时间晚于 t 的版本。如果需求依据业务生效时间,却用加载时间建历史,迟到数据就会被静默归入错误版本。
面试官也会检查生产行为。相同批次重跑不能再造一个版本;不完整快照不能把几千万客户判成删除;关闭当前行与插入替代行必须原子完成;迟到修正可能需要拆分历史区间,而非只改当前行。如果业务既要查询“值何时生效”,又要查询“仓库何时得知”,普通 SCD Type 2 不够,需要双时态模型。
回答前先澄清的问题
- 哪些属性会影响历史分析? 只跟踪已约定的 Type 2 字段。审计字段和摄取时间不应创建业务版本。
customerid是否稳定且永不复用? 它是业务键,每个历史版本另有独立的customersk代理键。- 源端是否提供可信生效时间或序列? 每日快照只能证明何时观察到某个值,不一定能证明它何时生效。没有可靠源时间,仓库不能凭空创建正确的回溯边界。
- 每份快照是否有明确的完整标记? 只有完整性、行数和控制总数检查通过后,才可把缺失解释为删除。部分抽取不是删除流。
- 删除应表示什么? 本方案关闭活动版本,并插入
is_deleted = true的当前墓碑版本,显式保留删除边界。 - 事实表是在摄取时保存
customer_sk,还是查询时按时间关联? 事实加载时解析代理键可简化后续查询;时间关联仍适合回填和校验。 - 修正是否允许重写过去的业务历史? 若允许,必须保留原始源版本和审计记录,因为回填可能合理改变旧报表结果。
- 是否同时保留知识时间和业务时间? 若审计需要两者,应分别建模有效时间与系统时间,而非强塞进一个区间。
30 秒回答框架
我会先定义业务键、Type 2 属性、可信生效时间和删除策略。每个版本使用独立代理键和
左闭右开的 [valid_from, valid_to) 区间,每个客户只有一个当前版本。我先暂存并去重
一份已验证完整的快照,只对跟踪属性计算哈希,再把记录分为未变化、新增、变化和删除。
属性变化时,在同一个原子操作中关闭旧版本并插入新版本;批次 ID 和源版本保证重跑幂等。
事实表按订单发生时间解析有效版本。迟到修正需要拆分受影响的历史区间;发布前拒绝当前行
重复、区间重叠、事实外键错误和异常删除激增。分步拆解
第一步:加载前先定义粒度和字段
粒度是“一个客户在一个有效区间内的一个版本”。实用表结构可以包含:
dim_customer {
customer_sk // 当前版本的代理主键
customer_id // 源端持久业务键
segment
sales_region
valid_from
valid_to // null 表示开放结束
is_current
is_deleted
change_hash // 只包含 Type 2 属性的规范化哈希
source_version
load_batch_id
}全链路统一使用 [validfrom, validto)。如果变化在 2026-07-10T09:00:00Z 生效,旧行就在该时刻结束,新行也从该时刻开始。左闭右开约定使边界只属于一个版本。时间戳还要统一时区和精度;混用日期、本地时间与 UTC 会制造假缺口或重复匹配。
业务键把多个版本归为同一实体,代理键标识一个不可变的历史状态,并由事实表保存为外键。iscurrent 只是查询便利字段,不能成为另一套真相,必须与 validto IS NULL 一致。哈希只是比较优化。应先规范化空值、类型、Unicode 和列顺序,同时保留真实跟踪列用于解释与审计。
第二步:估算扫描和写入路径
每日快照需要扫描 5000 万条源记录。变化率为 1% 时:
50,000,000 × 1% = 500,000 个新版本/天
500,000 × 365 = 182,500,000 个新版本/年
182,500,000 × 300 字节 ≈ 54.75 GB/年的原始行数据这里要分清扫描成本与变化写入成本。快照只暂存一次,只投影必需列,再按业务键与维度当前行比较。分区或聚簇应服从真实查询与维护模式,例如业务键用于查找、有效时间用于裁剪;不要机械创建数千个很小的日分区。索引、列式压缩、保留周期和回填放大都应使用接近生产的数据实测。
第三步:让普通批次确定、原子且幂等
使用不可变的 snapshotid 落地抽取结果。触碰目标表前,检查完成标记、模式、业务键唯一性、行数和控制总数。按 customerid 使用可信源版本去重;若两行排序等级相同但内容不同,应把它当成输入错误,而非随意挑一行。
规范化跟踪属性,并将每条暂存记录与目标表当前版本比较:
- 没有当前业务键:插入第一个版本。
- 跟踪值相同:不做任何事;刷新
loaded_at不应制造历史。 - 跟踪值不同:在可信生效时间关闭当前行,并插入新的当前版本。
- 当前目标键不在已验证完整的快照中:关闭该行,并插入当前墓碑版本。
每个变化键的关闭与插入必须位于同一个目标事务或原子表操作中。当前版本唯一约束可以阻止两个并发加载器同时造版本。记录 snapshotid、loadbatchid 和 sourceversion,已应用的源版本应被拒绝或识别为无操作。这样即使提交后的确认丢失,重试也只会收敛,不会重复历史。
第四步:按业务事件时间解析事实
加载订单时,使用订单业务时间解析客户版本,再把 customer_sk 固化到事实表。一个与具体产品无关的时点查询形态如下:
SELECT d.customer_sk
FROM dim_customer AS d
WHERE d.customer_id = :customer_id
AND d.is_deleted = false
AND d.valid_from <= :order_time
AND (d.valid_to > :order_time OR d.valid_to IS NULL);不等式表达左闭右开区间。查询必须恰好返回一行。零匹配需要明确的未知成员或隔离策略;多行匹配则证明历史已损坏。迟到事实应使用事件时间,而非摄取时间。Kimball 的 Type 2 模式让事实保存版本代理键,正是为了使已加载事实保留当时有效的历史画像。
第五步:把迟到修正当成区间手术
假设仓库现有记录是 7 月 1 日起为西雅图、7 月 12 日起为丹佛;7 月 14 日收到可信修正,说明丹佛实际从 7 月 10 日起生效。只更新当前行会让 7 月 10 日和 11 日仍然错误。应找到包含 7 月 10 日的版本,在 7 月 10 日关闭西雅图,并把丹佛的开始时间移到 7 月 10 日。如果修正在既有区间中引入第三个值,就拆分该区间并保留下一个已知边界。
按源版本顺序应用修正,并对同一业务键加锁或串行化。保留原始输入和修正审计,包括旧区间、新区间、源版本、批次和原因。如果业务约定要求事实外键反映修正后的历史,就重新解析受影响时间窗内的事实。
这里存在信息上限:7 月 14 日首次观察到的快照,不能自行证明某值在 7 月 10 日已经生效,除非源端携带可信业务时间或其他有序记录。此时应使用 7 月 14 日作为观察时间,或隔离等待确认;静默回填到 7 月 10 日是在制造虚假精度。如果仓库同时需要 7 月 10 日的有效时间和 7 月 14 日的知悉时间,就增加第二套系统时间区间,并明确称为双时态。
第六步:验证不变量与运行护栏
每个批次完成后、发布前检查:
- 每个代理键唯一;
- 每个业务键恰好有一个当前版本,已删除业务键也保留一个墓碑当前版本;
iscurrent与开放的validto一致;- 同一业务键的区间不重叠,有结束时间的行都满足
validfrom早于validto; - 未变化的源值不新增版本;
- 相同源版本或批次重放不新增任何行;
- 每个非未知事实代理键都引用事实事件时间有效的维度行;
- 新增、变化、未变化与删除数量能与暂存快照对账。
监控快照完整性、重复键、变化率、删除率、版本增长、被拒绝的迟到修正、事实零匹配与多匹配、加载延迟和重跑差异。若突然消失 4000 万个业务键,应在它们变成 4000 万个墓碑前失败关闭。测试至少覆盖新键、相同快照重放、普通变化、删除后重建、迟到 3 天的修正、乱序源版本、部分快照,以及关闭旧行和插入新行之间的崩溃。
高质量示例回答
“我会把粒度定义成每个客户版本一行,而不是每个客户一行。customerid 是持久业务键,每个版本都有新的 customersk。Type 2 字段需要与分析人员约定,例如客户分层和销售区域;加载时间或格式修正不会产生历史。每行使用左闭右开的 [validfrom, validto),每个客户恰好有一行当前版本。
源端每天扫描 5000 万行,约 50 万行发生变化。这意味着每年约新增 1.825 亿个版本;按每版本 300 字节举例,物理开销前的原始增长约为 54.75 GB。我会只暂存一次快照,并只把它与维度当前行比较,避免每次连接全量历史。
每份快照都有稳定 ID,且必须通过完成标记、业务键唯一性、模式、行数和控制总数检查。只对 Type 2 跟踪列做规范化哈希。新键直接插入;哈希相同不处理;发生变化时,在一个原子操作中关闭旧区间并插入新版本。只有经过验证的缺失键才会关闭旧版本并插入删除墓碑。源版本和批次 ID 保证幂等,当前行唯一约束阻止并发重复版本。
订单通过 validfrom <= ordertime 且 validto > ordertime 解析 customer_sk,空结束时间表示仍然有效。查询必须返回一个未删除版本。迟到事实使用订单时间,而不是到达时间。迟到的维度修正应定位到可信生效时间所在区间,拆分或调整该区间,并保留之后的已知边界。如果源端只有到达时间,我不会虚构提前 3 天的生效时间;如果业务既要业务时间又要仓库知悉时间,我会提出双时态模型。
发布前,我会检查每个业务键只有一个当前版本、区间无重叠、边界有效、事实引用正确、源目标数量可对账,并验证相同输入重跑时零新增。异常删除率或变化率会触发阻断,原始快照和修正审计则保证回填可解释、可回放。”
常见错误
- 对全部源字段计算哈希 → 审计时间戳会制造无意义版本 → 只对约定的 Type 2 属性做规范化哈希。
- 把自然键当维度主键 → 多个历史版本发生键冲突 → 用业务键归组,用代理键标识每个版本。
- 混用包含结束端点的区间 → 变化边界上的事实匹配两行 → 全链路统一
[validfrom, validto)。 - 把摄取时间当生效时间 → 迟到更新和事实关联到错误状态 → 使用可信业务时间,并说明缺失时的降级语义。
- 把快照中每个缺失行都判为删除 → 部分抽取可能清空维度 → 应用缺失语义前要求完成标记和异常检查通过。
- 分两次提交关闭与插入 → 崩溃后会留下零个或两个当前版本 → 原子完成版本切换,并约束当前行唯一。
- 每次重跑都创建版本 → 重试膨胀历史并改变旧结果 → 持久化源版本和批次标识,让相同输入成为无操作。
- 迟到修正只改当前行 → 较早事实仍挂在错误状态 → 拆分或调整受影响历史区间,并重新解析对应事实窗口。
- 看到两个时间戳就称为双时态 → 两者语义仍不清楚 → 明确有效时间和系统时间,并分别定义区间与修正规则。
- 只检查行数 → 区间重叠和重复当前行仍可漏过 → 同时检查时间、引用、幂等和对账不变量。
延伸追问与回答
追问一:为什么不把所有属性都做成 Type 1?
Type 1 适用于旧值没有分析意义的字段,例如拼写修正。它会销毁旧值,因此无法回答“这笔订单当时属于哪个区域”。应按报表语义分类属性。同一维度可以让部分列使用 Type 1、部分列使用 Type 2,但更新规则必须明确。
追问二:当前行应该用 valid_to = NULL,还是远未来哨兵值?
两者都可以。NULL 能直白表达“开放结束”,但查询需要处理空值;最大支持时间之类的哨兵值可以简化范围过滤,却可能泄漏到报表,或超出另一个引擎的日期范围。选择一种表示,让 is_current 与之保持一致,并在所有查询和连接器中验证同一约定。
追问三:删除后又使用同一业务键重建,怎么办?
在删除时间关闭活动业务版本,并添加删除墓碑;重建时关闭墓碑,再插入带新代理键的活动版本。还要先确认源端确实在复用同一个实体身份;若这个标识被分配给了另一个人,就应引入能区分实体的持久键。
追问四:源端没有可靠的 updated_at 怎么办?
像快照工具的 check 策略一样,明确列出需要规范化比较的跟踪列。生成的区间从仓库观察到变化时开始,未必等于真实业务变化时间,必须记录这一限制。如果必须得到准确业务生效历史,就获取源序列、审计日志、CDC 流或领域事件,而非编造时间戳。
追问五:它和 CDC 管道有什么区别?
CDC 回答发生了哪些已提交新增、更新和删除,以及它们的源端顺序。SCD Type 2 回答所选维度属性如何成为历史版本,以及事实表应引用哪个代理键。CDC 流可以驱动 SCD 加载器,快照也可以。可靠捕获本身不会防止有效区间重叠或错误时点关联。
追问六:什么时候应避免 Type 2 的行数增长?
不需要历史分组的高频属性、大型自由文本,以及更适合表达为事件或事实的运行状态,都不适合直接做 Type 2。根据查询需求,可以改用 Type 1、独立迷你维度、累积或周期事实,或专门历史表。决策依据是分析语义和实测读写成本。
追问七:如何证明迟到回填没有破坏历史?
先把回填结果写入暂存区或影子表,按业务键比较区间集合,并报告新增、拆分、缩短、延长和删除的版本。断言无重叠、只有一个当前行、未受影响的代理键稳定,并且事实键只在预期修正时间窗内变化。保留原始源版本和批次清单,用于回放与回滚。