主题:第13部分 增量、快照与历史
难度等级:中级
预计学习时间:2-3 小时
先修知识: 对 dbt(data build tool)和数据模型的基础理解
具备 SQL 编写查询与理解窗口函数的水平
理解数据仓库与 ETL/ELT 流程的概念
熟悉银行报告的业务领域(客户、付款、贷款、授权)
对数据可复现性概念的基础理解
学习目标: 区分三种数据历史模式(全量刷新、增量、快照),并理解何时应使用哪种模式
为包含历史的模型撰写正确的规范,包括键、事件日期与重放策略
确定可证明模型重放安全性的一组验证事实
分析延迟到达数据的需求,并提出契约层面的约束
应用所学知识,在银行业务线中选择历史策略(客户、卡片、贷款、授权)
概述:数据历史不仅仅是技术细节,而是数据源与数据消费者之间的“时间契约”。在银行报告中,若无法恢复昨日的表状态,审计将无法进行,并会动摇对模型的信任。本章将三种历史模式(全量刷新、增量、快照)作为不同的“时间承诺”加以介绍,阐明包含历史的模型规范应包含哪些内容,描述最低限度的验证事实与重放策略。特别关注延迟到达的数据——它会改变结果的可复现性,因此必须在编写 SQL 之前予以说明。读者将学会在生产环境中识别何处需要历史,并以明确的决策取代“由代理自行决定”的做法。
核心概念: 全量刷新(Full refresh):每次运行时从数据源完全重建表的模式。它提供了一份简单、始终更新的“当前”视图,但不保留昨日的状态。适用于教学示例、较小的表以及不常变动的字典表。在生产环境中,如果表很大且依赖其他模型,这种模式会变得昂贵且不安全。
增量模型(Incremental model):dbt 根据唯一键与日期窗口,仅添加或更新发生变化的行的模式。该模式需要明确的策略(insert、merge、delete+insert)、唯一键以及重放策略。它节省资源,但带来隐含的义务:如何处理延迟到达的行、窗口应保留多久、是否可以重新计算过去。
快照(Snapshot):按时间存储属性变化的模式。每一行版本都会获得一个动作时间戳(dbt_valid_from、dbt_valid_to),从而可以回答“我们昨天知道的是什么”。适用于状态、分群、授权以及变更缓慢的维度,在这些场景下审计可追溯性至关重要。
唯一键(Unique key):在模型中唯一标识一行的属性或属性组合。没有明确的键,既无法进行更新,也无法正确地建立快照,更无法进行重复检查。键必须在时间维度上保持稳定,并且不应依赖于可变的业务字段。
事件日期(Event date):用于确定事实所对应时间点的字段。它与行的加载日期不同:操作可能发生于昨日,但今日才进入系统。事件日期是重放窗口、增量模型筛选与快照 SCD 逻辑的基础。
重放策略(Replay policy):对是否可以重算过去以及覆盖哪些时间段的明确描述。若没有重放策略,模型的任何一次重新运行都可能在不知不觉中改变历史聚合值。该策略回答以下问题:哪些行允许被追溯修改,哪些行需要 PatchSpec 并经评审者确认。
延迟到达的数据(Late-arriving data):其事件日期早于增量模型中最新日期的行。它们的处理是潜在语义漂移的主要来源。相关决策(是否将其纳入聚合、覆盖哪个时间窗口、对已生成的报表如何处理)不能留给 is_incremental() 内部处理——这是一个契约层面的问题。
Patchspec:对已记录的历史事实进行修改的正式申请。当延迟到达的修正涉及超出重放窗口的时段时使用,并需要评审者确认与对后果的明确说明。
验证事实(Validation facts):用于证明模型历史正确性的最小断言集合:重复运行 dbt build 在 raw 未变更时不会改变结果;唯一键不重复;事件日期非空;重放窗口已被描述;评审者已人工确认历史语义。
可复现性(Reproducibility):模型的一种属性,即其结果可基于固定的输入数据集与规范予以恢复。任何对过去的隐式重写(包括对需要历史的表执行全量刷新)都会破坏该属性。
重要日期: 巴塞尔协议 iii(2010-2011):加强对银行报告可复现性的要求,包括风险报告与客户细分,提升了快照方法的重要性
GDPR 生效(2018年5月25日):强化了对客户授权记录与审计的要求,使授权历史从“可选项”变为“必备项”
IFRS 9 标准(自 2018 年起分阶段引入):要求按时间追踪信用风险阶段(Stage 1/2/3)与逾期情况,这离不开明确的历史记录
152-фз 法规(自 2014 年起生效):俄罗斯个人数据法,要求以历史视角保存处理与授权信息
练习题: 标题:为数据源选择历史策略
题目:给定一张表 raw.transactions,包含 transaction_id、client_id、transaction_date、amount、status、load_dttm 等列。30 天内表内约有 500 万行,每日新增操作的平均量为 150,000 条。操作状态可能在事件发生后 14 天内被追溯修改(例如从 PENDING 变为 CONFIRMED 或 DECLINED)。请确定合适的历史策略(全量刷新、增量或快照),记录唯一键、事件日期、重放策略以及延迟到达数据的处理规则。
解答:1. 选择增量模型,因为对 500 万行执行全量刷新成本过高,而快照则显得冗余——我们关心的是操作的当前状态,而非其状态变更历史。2. 唯一键为 transaction_id(稳定且不依赖于状态)。3. 事件日期为 transaction_date,而非 load_dttm。4. 重放策略:允许更新 transaction_date 落在距当前日期最近 14 天之内的行;超出该范围的行需要通过 PatchSpec 进行修改。5. 延迟到达的数据:若 load_dttm 为今日而 transaction_date 早于 14 天,则接受该行,但在评审者确认前不会重新计算该日的聚合值。6. 增量策略:按 transaction_id 进行 merge,在 is_incremental() 中按 transaction_date >= current_date - 14 进行过滤。7. 验证事实:重复运行 dbt build 在无新 raw 时不会改变结果表;transaction_id 的唯一性得到保证;transaction_date 非空。
难度:中级
标题:为包含历史的模型撰写规范
题目:将一段糟糕的规范表述改写为良好的表述。糟糕的表述:“为模型 client_segments 做增量加载。” 补充背景:该模型存储客户的分群(MASSE、VIP、STANDARD、RISK),分群按季度评估,但在重大事件(违约、向监管投诉)发生时可能更频繁地变更。
解答:糟糕的表述:“做增量加载。” 良好的表述:“模型 client_segments 使用 client_id 作为唯一键,使用 segment_effective_date 作为事件日期。客户的分群在每个 segment_effective_date 上确定。季度内发生的变更(违约、向监管投诉)可以追溯更新客户记录,但仅限于本季度内。对过去季度的变更需要通过 PatchSpec 进行,并经评审者确认。为了按时间审计分群,使用 snapshot client_segments_history,包含字段 dbt_valid_from 与 dbt_valid_to,其中快照键为 client_id,策略为 timestamp。历史语义在首次发布前已由风险报告负责人确认。”
难度:中级
标题:诊断历史相关问题
题目:风险报告团队抱怨:“昨天仪表盘上客户还处于 VIP 分群,今天就没有了。” 模型 client_segments 以全量刷新模式运行。发生了什么?模型本应如何构建?
解答:发生了什么:全量刷新重建了表,客户的当前分群发生了变化(例如,客户不再满足 VIP 的条件)。之前状态的历史未被保留。仪表盘仅显示当前快照,无法回答“昨天我们知道的是什么”。解决方案:模型 client_segments 本应采用 snapshot(包含字段 dbt_valid_from、dbt_valid_to,键为 client_id),或采用包含完整变更历史的增量模式。对于分群的分析型数据集市,正确选择是 snapshot,因为分群变更不频繁,但每一次变更都对审计意义重大。仪表盘应在 dbt_valid_from <= report_date < dbt_valid_to 的过滤条件下访问数据集市,而非访问当前表。此外,还需在规范中描述重放策略:是否可以追溯修改过去时段的客户分群,或者此类修改需要 PatchSpec。
难度:中级
标题:用于模型审计的 Qwen 查询
题目:编写一条向 Qwen 助手发起的查询,用于在你的 dbt 项目中找到所有“需要历史但未做描述”的模型。请说明助手应为每个此类模型返回哪些信息,以便结果可转化为任务项。
解答:查询文本:“读取 specs 与 marts,找出需要历史、增量策略或 snapshot 的模型。对每个模型返回键、事件日期、重放策略与未决问题。不要修改 SQL。” 助手应返回如下形式的表格:model_name | unique_key | event_date | history_strategy | replay_window | late_data_policy | open_questions。例如,对于 client_segments:client_id | segment_effective_date | snapshot | N/A(本季度内) | 本季度内的变更——可以;更早——仅可通过 PatchSpec | “未描述 snapshot 策略”、“缺少关于可复现性的验证事实”。基于此回答,可以形成任务待办:补充规范、实现 snapshot、为键的唯一性与事件日期非空编写测试、与模型负责人对齐策略。
难度:初级
案例研究: 标题:Open API 中客户授权历史的丢失
场景:一家大型俄罗斯银行为合作伙伴服务(保险公司、券商、交易平台)部署了 Open API。每次合作方请求都需要客户存在有效的数据传输授权。授权存储在模型 consents 中,该模型以全量刷新模式运行:每次运行都会完整重建表,表中仅保留最新状态(active 或 revoked)。
挑战:2024 年 6 月,一位客户向中央银行投诉,称合作方券商在未经其许可的情况下将其数据传输给了保险公司。银行试图恢复历史:“2024 年 5 月 15 日合作方发起请求时,哪一项授权是有效的?” 结果发现,5 月 15 日的 consents 表已被重建——当前版本仅显示今日的状态。虽然审计日志存在,但它未通过唯一键与事件日期与模型关联。监管机构要求恢复 6 个月的历史,但数据并不存在。与此同时,风险部门发现,3 月的若干操作状态为“授权有效”,而实际上客户已在 2 月撤销——全量刷新在过去所有时段将 “revoked” 替换为 “active”。报告的可复现性遭到破坏,审计无法进行。
解决方案:数据团队在 6 周内完成了以下变更。1) 创建了 snapshot 模型 consents_history,以 (client_id, partner_id, scope) 为唯一键,按 updated_at 字段使用 timestamp 策略;表中现存储 dbt_valid_from / dbt_valid_to 对。2) 当前数据集市 consents 仍保持全量刷新,但被显式标注“不得用于审计历史”。3) 为每个包含历史的模型撰写了规范:键、事件日期(consent_updated_at)、重放策略(允许在最近 30 天内进行追溯修改;超出范围仅可通过 PatchSpec 修改,并需合规确认)、延迟到达数据的处理规则(若 updated_at < 30 天则接受;若更早则记入异常,且不重新计算聚合值)。4) 在 snapshot 中引入了唯一键测试以及“重复运行 dbt build 在无新 raw 时不改变结果”的测试。5) 通过审计日志完成了 6 个月的历史恢复,并对 200 名随机客户进行人工核对;恢复准确率为 99.4%(0.6% 的差异已说明并记录在案)。
结果:银行向监管机构提交了恢复后的授权历史以及详尽的存储架构报告。由于透明度与响应迅速,罚金得到减轻。事件之后,团队确立了规章:“任何包含客户状态或分群的模型,若未明确历史策略与验证事实,不得发布。” 在接下来的一个季度中,相同的方案被应用于 client_segments、credit_risk_stages、card_status 等模型——这些模型此前均采用会丢失历史的全量刷新。响应监管机构的历史恢复请求的平均时间从 3 周缩短至 2 天。
经验教训: 仅设置 materialized='incremental' 本身并不能解决历史问题——它只改变了写入方式。历史的含义由键、事件日期与重放策略决定
全量刷新仅适用于“过去无关紧要”的模型;任何涉及状态、授权与分群的内容,都需要 snapshot 或具有明确历史的增量模型
延迟到达的数据是一个契约层面的问题,而非技术问题:不能将其处理留给“is_incremental()”,必须明确描述窗口与例外
报告的可复现性是系统整体的一种属性;即便各张表都正确,若它们在时间维度上的关联被破坏,审计将无法进行
包含历史的模型规范应回答 5 个问题:键、事件日期、是否可重算、延迟数据的策略、证明重放安全性的团队
相关概念: 快照
增量模型
延迟到达的数据
重放策略
验证事实
包含历史的模型规范
标题:延迟到达的支付与被破坏的聚合
场景:一家零售银行计算每日卡片操作聚合:交易额、平均客单价、拒付比例。聚合由增量模型 daily_card_aggr 构建,该模型于凌晨 02:00 取昨日所有操作并更新相应的聚合。唯一键为 operation_id,事件日期为 operation_date,重放窗口为 3 天。
挑战:6 月 10 日上午,反欺诈监控部门发现 6 月 5 日的报表中遗漏了一笔大额操作(180,000 卢布),该操作直到 6 月 9 日才落地。原因在于,在 6 月 5 日至 6 月 6 日的夜间,处理流程返回了一笔被延迟的操作,而 6 月 6 日的 dbt 构建已不再重算 5 日的聚合(已超出 3 天窗口)。反欺诈监控部门基于不完整的数据做出了决策。与此同时,类似情况也发生在 6 月 7 日的操作上——该操作于 10 日到达:7 日的聚合被更新,但 8 日与 9 日的聚合缺少相邻的上下文操作。团队无法解释“上周的聚合从根本上是否正确”。
解决方案:团队重新审视了重放策略。1) 将重放窗口从 3 天扩展到 7 天,用于大额操作(amount > 100,000 卢布),并在 dbt 模型中新增分支:“if amount > threshold, 将窗口扩展至 7 天”。2) 对于超出窗口的延迟到达操作,建立了独立的暂存层 late_operations,该层不会修改聚合,而是进入异常报告。3) 模型规范中补充了一项条款:“事件日期早于 7 天的操作,仅可通过 PatchSpec 加入聚合,并需注明原因且经分析师确认”。4) 引入了每日测试:“raw 中最近 7 天的操作总金额等于聚合中相同 7 天的操作总金额”,可在消费者注意到之前捕获差异。5) 向反欺诈监控部门开放了 late_operations 的优先访问权限。
结果:在接下来的 3 个月内,记录到 47 起超过 7 天的延迟到达操作案例。全部按规章处理:38 起通过 PatchSpec 纳入聚合,9 起被认定为处理流程错误并被驳回。反欺诈监控部门不再抱怨“丢失”的操作,并开始信任报表。对重大异常的平均响应时间从 24 小时缩短至 6 小时。团队总结出原则:“重放窗口应与数据源最大合理延迟对齐,而不是为了便利 dbt 构建。”
经验教训: 重放窗口的大小是一项契约层面的决策,而非技术参数;它应反映数据源的实际延迟,而不是节省构建时间的诉求
延迟到达的数据不应“默默丢弃”,而应被显式记录在独立的一层,并送达报告负责人
PatchSpec 不是“错误的补丁”,而是通过明确评审者责任来追溯修改的正常机制
验证事实应包括 raw 与 marts 之间的核对,而非仅限于唯一性与非空值的测试
历史语义(是否可以修改过去某日的聚合)比写入方式(merge、insert、snapshot)更重要
相关概念: 增量模型
延迟到达的数据
重放策略
PatchSpec
验证事实
学习建议: 从三种历史模式(全量刷新、增量、快照)的对比表开始,并为每种模式用一句话概括其“时间承诺”——这有助于在实践中快速选择策略
尝试针对自己项目中每个模型回答规范中的 5 个问题(键、事件日期、是否可以重算过去、如何处理延迟数据、证明重放安全性的团队)——若任何一个问题无法回答,则历史未被描述
不要混淆事件日期与加载日期(load_dttm):这是两个不同的量;增量模型应按事件日期进行过滤,否则将丢失延迟到达的数据
按照本章模板,针对自己的项目向 Qwen(或任何 LLM 助手)构造查询——它将快速给出未描述历史的模型切片,并将抽象的主题转化为具体的待办
在项目中按本章模板(客户属性、卡片操作、贷款、Open API 授权、重放与延迟数据)维护一份单独的《历史策略》文档,并在每次发布涉及客户状态或分群的新模型时进行更新
将验证事实编写为可执行的检查,而非一段文字描述:键的唯一性测试、“重复 dbt build == dbt build” 测试、raw 与 marts 在窗口上的求和对账
在增量与快照之间选择时,思考的不是表的大小,而是消费者的问题:若问“现在是多少”,选择增量;若问“昨天是什么”,选择快照
常见陷阱:设置 materialized='incremental' 就认为历史问题已解决。务必自问:“如果 raw 中出现一条针对上个月的行更新,会发生什么?”——若未给出回答,则历史问题未解决
学会批判性地阅读规范:“做增量加载”这类表述是一个危险信号,需要对全部 5 个要点加以澄清
为应对测验题目,结合自己项目中的真实示例逐一演练:增量 vs 快照、重放安全性事实、延迟数据的契约 vs SQL
扩展资源: dbt 官方关于增量模型的文档:https://docs.getdbt.com/docs/build/incremental-models — 关于 is_incremental() 配置、merge/insert/delete+insert 策略、唯一键与日期窗口的官方指南
dbt 官方关于快照的文档:https://docs.getdbt.com/docs/build/snapshots — 关于 SCD Type 2、timestamp 与 check 策略、dbt_valid_from / dbt_valid_to 字段、snapshots 依赖图的指南
《fundamentals of data engineering》(Joe Reis、Matt Housley):关于渐进式加载与 CDC 的章节,包括延迟到达数据与重放窗口的模式
《data quality: the accuracy dimension》(Jack Olson):关于数据可复现性与审计的章节,可应用于银行报告
IFRS 9 标准(МСФО 9):https://www.ifrs.org — 标准官方文本,阐释了对信用风险阶段与逾期历史的记录要求
巴塞尔协议 iii:巴塞尔银行监管委员会(BCBS)发布的文件——对风险报告可复现性的要求
dbt-utils 包:https://github.com/dbt-labs/dbt-utils — 开箱即用的测试与宏,包括 surrogate_key、非空值检查与模型间核对
dbt labs 博客《how we structure our dbt projects》:关于 staging / intermediate / marts 分层及层间契约描述(包括历史)的章节
摘要:数据历史是一种时间契约,而非实现层面的技术细节。本章将三种模式(全量刷新、增量、快照)作为不同的“时间承诺”:重建当前状态、增/改变化内容、保留属性变更。其选择应在 SQL 编写之前记录在规范中:由哪个键标识行、哪个字段是事件日期、是否可以重算过去、如何处理延迟到达的数据、由哪个团队证明重放的安全性。延迟到达的数据是潜在语义漂移的主要来源,其处理属于契约层面,而非 is_incremental()。在银行业务线中,客户属性与分群、卡片操作、贷款阶段与 Open API 授权均必须有历史记录。最低限度的验证事实(重复运行 dbt build 不改变结果、键的唯一性、事件日期非空、窗口已描述、评审者已确认)将历史从“意图”转化为模型可度量的属性。最关键的实践结论是:每个项目都应存在《历史策略》文档,并针对每个实体明确记录策略,即便最初版本仍是简单的全量刷新。