深入解锁 dbt——Snapshots:数据快照

26 阅读44分钟

在你处理数据的过程中,很可能遇到过某种 dataset:它会变化,但历史并没有被保留下来。这可能会变成一场噩梦,因为 data professionals 经常需要查询某个 point in time 的数据。在本章中,我们将讨论 dbt 如何使用 snapshots 帮助跟踪 row changes,并维护 change history。如果你熟悉 dimensional data modeling,可能也熟悉 slowly changing dimensions(SCDs),它们允许你以不同方式,或者说不同 Types,跟踪 historical data。Snapshots 与 Type-2 SCD 完全一致:current rows 和 changed rows 都会作为 records 存储在同一张 table 中,并通过 status 和 timestamp columns 进行区分。在其他 data model designs 中,这个概念也存在,只是可能有不同名称。

考虑一个场景:你的 data warehouse 中有一些 tables 来自公司的 application database,这些 tables 中的数据经常被 updated,但 history 没有被维护。我们可以把这种 data 描述为 mutable。它对 software developers 来说很好,但对 data engineers、data analysts 和 analytics engineers 来说并不理想。Mutable data 对 software developers 很理想,因为他们通常只关心数据的 current state。然而,从 data analytics 的角度查询可能变化的数据时,记录单行数据的变化历史是非常有价值的,它可以解锁很多 insights,而如果我们只存储 current state,这些 insights 可能就会丢失。

主要在 transactional systems 中,虽然有时 analytical systems 中也会发生,teams 需要跟踪 changes。无论是为了 auditing、troubleshooting,还是 replication,teams 都会使用 change capture technologies。大多数主流 database technologies 都内置了 change capture,并且通常会在 system tables 中跟踪 changes,随后可以读取这些 system tables 来判断发生了什么变化。从 SCDs 的角度看,这种方式更类似 Type-4:historical changes / records 存储在独立 table 中。虽然不完全相同,但使用 change capture 可以产生与 dbt snapshots 类似的 tracking 效果。

看一下表 5-1 和表 5-2,它们展示了一个 customer table 的示例:data 发生了变化,但 history 没有被保留下来。在这些示例中,我们看到 customer 的居住州从 New Hampshire 变成了 Florida。

表 5-1:住在 New Hampshire 的 customer 示例

CustomerIdStateUpdatedAt
100New Hampshire2021-01-30 12:00:00

表 5-2:Customer 100 从 New Hampshire 搬到 Florida

CustomerIdStateUpdatedAt
100Florida2023-01-31 05:30:00

如果 data 以这种格式存储,我们只能基于 current state 回答问题。但如果 data 以类似表 5-3 的格式存储呢?以这种格式存储 data,可以让你回答诸如这个 customer 在 New Hampshire 住了多久,或者他们下单时住在哪里等问题。当然,这个概念远不止跟踪 customer history,它几乎可以应用到任何 mutable data source。Snapshots 是 dbt 对“如何跟踪这些 changes”这一问题的回答。本章中,我们将讨论什么时候应该使用 snapshots、它们如何工作、如何配置它们,以及更多内容。

表 5-3:跟踪了 history 的 customer 示例

CustomerIdStateUpdatedAt
100New Hampshire2021-01-30 12:00:00
100Florida2023-01-31 05:30:00

When to Use Snapshots

在本章 introduction 中,我们从非常高的层面讨论了 mutable data sources 和 snapshots。稍后我们会讨论如何实现 snapshots,以及实现时涉及的 technical details。但首先,让我们更深入地探讨:在开发 data warehouse 时,什么时候最适合使用 dbt 的 snapshot feature,什么时候应该避免使用。

Snapshots 有一个主要 use case,可以概括为:让你能够跟踪那些不存储历史的数据源的变化。这是对 snapshot 功能的一个高度简化版本,但已经能表达核心意思。还需要指出的是,几乎总是建议对 sources 进行 snapshot,然后在 downstream modeling 中将 snapshot 当作 source 对待。也就是说,通常应该使用 select * from {{ some_source }} 来简单跟踪 source data 的变化。你可以在 snapshot 中实现 transformations,但不推荐这样做,因为 snapshots 本质上不是 idempotent 的。因此,如果你需要重新运行 transformation,无法保证每次产生的 resulting output 都相同。

Note

Idempotent,或 idempotence,是 Data Engineering 中常用的一个术语,用来描述某个 pipeline 或 transformation:给定一组 inputs,它始终会产生相同 output。

为了巩固你对 snapshots 概念的理解,我们来看几个可以使用 snapshots 的例子。

Example One

在这个例子中,让我们考虑本书 GitHub repo 中包含的 orders data。这是一个 mutable data source 的很好示例,因为它表示的是会变化的数据,并且在实践中可能相当频繁地变化。注意,该 dataset 中的 orders 属于三种 statuses 之一:placed、shipped 和 returned。可以想象,如果在每个 order 的 status 或其他 fields 被 updated 时,能够维护它的 history,那会非常有价值。这正是 dbt snapshots 可以帮忙维护 row changes history 的地方。

如果你想跟随这个示例操作,可以先运行 Listing 5-1 中的 query,它会返回 OrderId 21 的结果。可以看到,这个 order 的 status 显示为 returned。幸好,我们有两个 timestamps:OrderPlacedTimestampUpdatedAt,它们能让我们稍微了解 order 是什么时候 created,以及最后一次 updated 是什么时候。虽然通过这些 fields,我们知道 order 什么时候 placed 和 returned,但缺少它什么时候 shipped 的 details,因此丢失了有价值的信息。

select
  OrderId,
  OrderStatus,
  OrderPlacedTimestamp,
  UpdatedAt
from raw_orders
where OrderId = 21;

Listing 5-1:返回 returned order number 21 的 record 的 query

如果我们实现了 snapshots 来监控这张 table 中的 row changes,就能够保留这个 order 经历过的 status updates 及其发生时间的 history。这会解锁对 data 进行大量 analysis 的能力。例如,可以尝试判断从下单到发货之间的 lag time 是否与 returns 存在相关性。也许我们能够建议降低 lag time,从而减少 returns 数量。但如果没有跟踪 historical changes,就无法回答这些问题。

虽然你可以使用 snapshots 来跟踪 Orders table 的 changes,但由于这些 data 变化速度较快,在 dbt 之外有更好的方式来做这件事。本章后面会讨论 snapshots 的潜在问题,以及如何克服这些问题。现在,先看第二个例子,说明什么时候可以使用 snapshots 监控 row changes。

Example Two

在前一个示例中,我们讨论了使用 snapshots 跟踪 Orders table 中 data changes,但这张 table 中 data 的变化速度可能相当高。虽然在高 velocity data 的 table 上使用 snapshots 跟踪 changes 并不天然错误,但有更好的方式做这件事,我们稍后会讨论。现在来看一个在实践中使用 snapshots 跟踪 row changes 的更理想场景。

在这个示例中,我们会使用 raw_customers.csv file。回忆第 2 章,project 中的 raw_customers file 来自 customer relationship management system(CRM)。基于这一点,考虑以下场景。业务中的 salespeople 可以在 CRM 中修改 customer records,并且在与 customers 互动时会定期这样做,以确保我们拥有关于 customer 的最新信息。在 CRM 中,我们会跟踪 customer 的以下信息:name、phone number、email address 和 shipping address。可以想象,对大多数 customers 来说,这些 attributes 不会频繁变化。

这些不频繁变化、但仍然 mutable 的 datasets,被称为 slowly changing dimensions。更具体地说,dbt snapshots 实现的是 Type-2 slowly changing dimension(SCD),意味着 source dataset 中每条 record 的 history 都会被跟踪。注意,第一个词 slowly 暗示这些 data changes 的 velocity 是不频繁的。这正是 snapshots 最适合使用、也最可靠的场景,而 customer-related data 就是一个完美示例。

Note

Slowly changing dimensions 由 Ralph Kimball 推广,用来跟踪 source system 中 data 的 changes。这些 changes 发生得慢,但时间间隔有些不确定。

如果运行 Listing 5-2 中的 query,会注意到这个 customer record 创建于 2023 年 1 月 2 日,随后在 2023 年 1 月 9 日被 updated。基于当前 data state,我们不知道发生了什么 change,但假设这次 update 是 customer phone number 发生了变化。这里好的一点是,这次 change 发生在该 customer record 初始创建后的七天后,这已经足够 slowly changing,适合 snapshots 捕获这次 update。

select
  CustomerId,
  Phone,
  CreatedAt,
  UpdatedAt
from raw_customers
where CustomerId = 30;

Listing 5-2:返回 CustomerId 30 的 record 的 query

如果我们在这张 table 上实现了 daily snapshot,就会最终跟踪到该 customer phone number 的 changes。表 5-4 为你提供了 snapshot table 的视觉表示。注意,现在 CustomerId 30 有两条 records,第一行捕获 original phone number,第二行捕获 updated phone number。

Snapshot Meta Fields

如果你是第一次接触 Type-2 dimensions,前一节应该已经帮助你更好地理解它们,并理解什么时候适合通过 dbt snapshots 实现它们。不过,如果你之前用过 Type-2 dimensions,可能会好奇 dbt 如何实现你在 Type-2 SCD 中期待看到的 meta fields,例如 valid date ranges。幸运的是,这又是 dbt 帮助 streamline development process 的一种方式:作为 snapshot process 的一部分,dbt 会自动向最终 table 添加几个典型 metadata fields。

Why dbt Adds These Fields

处理 Type-2 SCDs 时,我们知道,对于 source table 中的每个 primary key,可能会有多行。回到本章前面的表 5-3,在这个 snapshot 中,CustomerId 100 有两条 records。但如果我们知道更多信息,处理这份 data 会简单很多:

  • 这两行中哪一行是 active?
  • 每一行在什么 date range 内 active?

虽然我们可以在 SQL 中添加一些 logic 来回答这些问题,但不需要这样做,因为 dbt 会向 snapshot 中的每一行添加四个 meta fields:

dbt_valid_from

row 被 inserted 时的 timestamp。这个 field 的计算方式会根据所使用的 snapshot strategy 而变化。

dbt_valid_to

row 变为 inactive 时的 timestamp。它实际上与新的 active row 的 dbt_valid_from value 相同。

dbt_scd_id

dbt 用于将 updates 和 inserts merge 到最终 table 中的 hash value。你很可能永远不需要使用这个 field。

dbt_updated_at

dbt 最近一次 updated 该 record 的 timestamp。它也由 dbt internal processes 使用,你不应该需要使用这个 field。

因此,dbt 会向 data warehouse 中的 table 添加四个 meta fields。但可以看到,你很可能只会使用前两个。dbt_valid_fromdbt_valid_to timestamps 是 Type-2 SCDs 中很多价值所在。这些 fields 允许你理解你正在处理的 record 在某个 point in time 的状态。表 5-4 扩展了本章前面 CustomerId 100 的示例,展示这些 meta fields 的样子。

表 5-4:带有 dbt meta fields:dbt_valid_from 和 dbt_valid_to 的 snapshot 示例

CustomerIdStatedbt_valid_fromdbt_valid_to
100New Hampshire2021-01-30 12:00:002023-01-31 05:30:00
100Florida2023-01-31 05:30:00null

可以看到,我们现在可以在 downstream models 中使用这张 table,基于某个 point in time 进行 analysis。也就是说,我们知道该 customer 从 2021 到 2023 年住在 New Hampshire,随后搬到了 Florida。此外,也可以通过只过滤 dbt_valid_to field 为 null 的 rows 来查询 active record。这只是 Type-2 SCDs 实践用法的冰山一角。本章后面会讨论如何在 downstream models 中使用这类 tables。

Monitor for Row Changes

到这里应该已经很清楚:snapshots 实际上是在 data warehouse 中构建 Type-2 slowly changing dimension。当然,dbt 让这个过程相当简单,并且不要求你实现这类 transformations 通常需要的 boilerplate code。相反,你可以让 dbt 帮你做这件事。当然,这仍然要求你理解 Type-2 SCDs 如何工作,这也是为什么在深入 implementation process 之前,我们先在本章开头讨论了几个 snapshots use cases。

dbt 提供两种不同 strategies,用于监控 data sources 中的 row changes:timestamp 和 check strategies。非常高层地说,timestamp strategy 顾名思义,是基于 source table 中的 timestamp 监控 record changes。这是最简单、最准确的 strategy,因此只要 source data 中有可靠 timestamp,它就应该是你的首选 strategy。第二个选项是 check strategy,它允许你定义一个或多个 columns 的列表,由 dbt 检查这些 columns 是否发生变化。可以想象,这种方法 implementation 会稍微复杂一些,但我们仍然会覆盖如何实现这两种 strategies,并提供一些可使用的 best practices。

Timestamp Strategy

在讨论两种 strategies 的 implementation process 时,我们会使用 raw_customers model,但会从 timestamp strategy 开始。如前所述,这种 strategy 是配置 snapshots 最简单也最推荐的方式。该 strategy 的工作方式是将 updated_at configuration 设置为 timestamp column 的名称。随后,dbt 会使用这个 timestamp 监控 data source 中的 row changes。让我们逐步看如何使用 timestamp strategy 配置 snapshot。这个示例中,我们会引用这个 file:customers_snapshot_timestamp_strategy.sql

为 snapshots 设置 .sql file 与其他 materializations 类型稍有不同,因为你必须用 snapshot Jinja block 包裹整个 transformation:

{% snapshot customers_snapshot_timestamp_strategy %}
...
{% endsnapshot %}

在构建 snapshots 的 workflow 中,我们建议始终从添加 snapshot block 开始。这不仅能避免你忘记添加 block 并遇到 errors,而且该 block 也是命名 snapshots 的方式。回忆第 4 章,对于其他 materializations,destination table 的名称会继承自 .sql file 的名称,除非显式配置。Snapshots 在这一点上不同,因为 destination table 的名称定义在 snapshot block 中。因此,在前面的示例中,这张 table 会被命名为 customers_snapshot_timestamp_strategy

接下来,你应该为该 model 添加 select statement。记住,你几乎总是应该 snapshot 一个已经在 project 的 sources.yml 中配置好的 single source。不过,为了示例目的,我们会从一个 single ref 中 select,因为我们的 sources 由 seed files 构成。此外,我们不建议在 snapshot query 中添加任何 transformations,而是直接 select * from source table。这会让 snapshots 保持简单,而且你始终可以在 downstream models 中添加 transformations。到这里,你的 snapshot file 应该像这样:

{% snapshot customers_snapshot_timestamp_strategy %}
select
  *
from {{ ref('raw_customers') }}
{% endsnapshot %}

刚才走过的两个步骤,对你构建的每个 snapshot 都应该执行,无论你使用 timestamp strategy 还是 check strategy。第三步,也是最后一步,是配置 snapshot 的具体内容,使 dbt 知道你希望如何监控 row changes。使用 timestamp strategy 创建 snapshot 时,有四个 configurations 必须定义:

target_schema

dbt 应将 snapshot materialize 到 database 中的哪个 schema。

strategy

snapshot 使用的 strategy,可以是 timestamp 或 check。

unique_key

dbt 应使用哪个 field 或 expression 来唯一标识 source table 中的一条 record。

updated_at

dbt 应使用哪个 timestamp 来识别 new 和 updated rows。

为了完成第一个 snapshot 示例,需要确定这些 configurations 的值。我们知道 source table 的 primary key 是 CustomerId,所以将它设置为 unique_key。此外,source table 中有一个 UpdatedAt field,因此将其设置为 updated_at。刚好我们的 timestamp field 与 dbt config 的名字相似,但在实践中,你的 timestamp field 可能并且很可能会不同。当然,strategy config 会设置为 timestamp,而 check strategy 会在下一个示例中讨论。最后,我们希望将 snapshots materialize 到一个独立 schema 中,所以会把 target_schema config 设置为将这些 tables 渲染到名为 snapshots 的 schema。现在可以将所有内容组合起来,并在 .sql file 顶部添加 config block。按下面方式设置 configurations 后,第一个 snapshot 示例就完成了:

{% snapshot customers_snapshot_timestamp_strategy %}
{{
   config(
     target_schema='snapshots',
     unique_key='CustomerId',
     strategy='timestamp',
     updated_at='UpdatedAt'
   )
}}
select
  *
from {{ ref('raw_customers') }}
{% endsnapshot %}

现在我们已经走完构建这个 snapshot 的过程,最后一步是执行 dbt snapshot command,让 dbt 在 database 中把它构建成 table。让 command 更受约束一些,使用 --select operator,只运行这个 snapshot。本章后面会更详细讨论 dbt 幕后实际做了什么。现在只需运行以下 command 来执行 snapshot:

dbt snapshot --select customers_snapshot_timestamp_strategy

你可以通过检查是否收到 dbt success message,以及检查 warehouse 中是否存在这张 table,来验证它是否成功:

dbt_learning.snapshots.customers_snapshot_timestamp_strategy

Meta Fields with the Timestamp Strategy

现在你已经运行了这个 snapshot,并且 table 已经 materialized 到 data warehouse 中,让我们看看 dbt 添加到这张 table 的 meta fields。回忆本章前面,dbt 会使用 dbt_valid_fromdbt_valid_to meta fields 添加 record validity time ranges。我们想特别指出 dbt 如何设置 dbt_valid_from field,因为两种 snapshot strategies 在这里有所不同。

当你使用 timestamp strategy 构建 snapshot 时,dbt 会使用你为 updated_at config 设置的 timestamp field 中的值。在我们的示例中,它配置为使用 ref_customers model 中的 UpdatedAt field。让我们走一个示例:查看 snapshot table 中的一条 active row,模拟 source 中的 update,重新运行 snapshot,然后看看 meta fields 如何受影响。

首先,你应该已经在 warehouse 中构建了这张 table。因此,如果运行 Listing 5-3 中的 query,它会返回表 5-5 中看到的结果。

select
  CustomerId,
  Phone,
  UpdatedAt
  dbt_valid_from,
  dbt_valid_to
from dbt_learning.snapshots.customers_snapshot_timestamp_strategy
where customerid = 1

Listing 5-3:返回 CustomerId 1 的 records 的 query

表 5-5:使用 timestamp strategy 时的 snapshot meta fields

CustomerIdPhoneUpdatedAtdbt_valid_fromdbt_valid_to
11-394-354-24582022-12-21 14:08:36.4642022-12-21 14:08:36.464null

注意表 5-5 中,dbt_valid_from field 使用了与 UpdatedAt 相同的值,因为正如前面指出的,这个 meta field 是基于你如何定义 updated_at config 设置的。这很容易理解,但让我们检查当 source data 发生变化时,dbt 如何更新 meta fields。由于示例中不是 live data,你可以运行下面的 query 来模拟对 raw_customers table 的 change:

update dbt_learning.public.raw_customers
set Phone = '1-394-354-2456',
    UpdatedAt = '2023-01-01 12:06:34.465'
where CustomerId = 1

这个 query 会更新 customer 1 的 phone number 和 update timestamp。虽然我们永远不建议在 production data 上这样 update,但在 learning environment 中这样做很方便。既然已经更新了 customer 1 的 phone number,就再次运行该 model 的 snapshot command:

dbt snapshot --select customers_snapshot_timestamp_strategy

成功完成后,你应该能够重新运行 Listing 5-3 中的 query,但注意现在 table 中 customer 1 有两行。一条 active record 和一条 inactive record 现在都存在于 table 中,结果应该类似表 5-6。

表 5-6:该 customer 的 phone number updated 后的 snapshot meta fields

CustomerIdPhoneUpdatedAtdbt_valid_fromdbt_valid_to
11-394-354-24562023-01-01 12:06:34.4652023-01-01 12:06:34.465null
11-394-354-24582022-12-21 14:08:36.4642022-12-21 14:08:36.4642023-01-01 12:06:34.465

可以看到,dbt 能够捕获该 customer 的 updated phone number,并更新 meta fields。表 5-5 中原本 active 的 record,现在在 dbt_valid_to column 中有了值。注意,该值与 active row 的 dbt_valid_from value 相同。这里的关键 takeaway 是:使用 snapshots 时,dbt 会使用你在 updated_at config 中设置的 column 来更新 meta fields。

Note

下一节会讨论 check strategy,但 dbt 设置 meta fields 的过程几乎相同,所以不会再次覆盖这些信息。关键差异在于,使用 check strategy 时,dbt_valid_from field 会使用 run time 时系统的 current timestamp 设置。

Check Strategy

虽然 timestamp strategy 应该是你实现 snapshots 时的首选方法,但并不是每个 data source 都会有一个可靠 timestamp 可用。当你需要 snapshot 这样的 data sources 时,可以使用 check strategy。使用这种 strategy 时,需要配置一个或多个 columns 的 list,dbt 会在 snapshot 运行时检查这些 columns 是否发生变化。

Caution

Check strategy 会生成一个 query,用 or keyword 在 where clause 中比较你配置的每个 column,以检查 changes。如果你检查 changes 的 columns 太多,可能最终得到 long-running snapshots。

遗憾的是,与 timestamp strategy 相比,check strategy 实现起来稍微不那么直接,因为你必须考虑希望监控哪些 column(s),以触发 snapshot table 中的新 record。根据我们的经验,有三种 approach 可以使用 check strategy 实现 snapshots。虽然使用 check strategy 本身并不是很理想,但我们认为这三种 approaches 有一个清晰的 progression,从最不理想到最理想,我们会按这个顺序覆盖它们:

  1. Check all columns
  2. Check columns in a list
  3. Check a hash column

在深入每种 approach 前,先看使用 check strategy 时需要设置的 configurations。注意,这些 configs 与使用 timestamp strategy 时必须设置的 configs 有细微不同:

target_schema

dbt 应将 snapshot materialize 到 database 中的哪个 schema。

strategy

snapshot 使用的 strategy,可以是 timestamp 或 check。

unique_key

dbt 应使用哪个 field 或 expression 来唯一标识 source table 中的一条 record。

check_cols

dbt 应检查哪些 columns 来识别 row changes。也可以设置为 all,表示检查 source 中的每个 column。

Check All Columns

如果你处在必须使用 check strategy 的情况,而且不知道从哪里开始,那么最简单的 approach 是配置 snapshot 检查 source 中每个 column 的变化。使用这种 approach 设置 check strategy 的步骤,与本章前面覆盖的 timestamp strategy 示例差别不大。关键差异来自 file 顶部的 config block。对于这个示例,可以参考文件:customers_snapshot_check_strategy__all.sql。如果你想在 data warehouse 中构建这个 snapshot,可以运行:

dbt snapshot --select customers_snapshot_check_strategy__all

注意,与 timestamp strategy 示例中的 config block 相比,这里的 config block 只有几处差异:

{{
   config(
     target_schema='snapshots',
     unique_key='CustomerId',
     strategy='check',
     check_cols='all'
   )
}}

与所有 snapshot 示例一样,我们会将这些 tables materialize 到 data warehouse 的 snapshots schema 中。由于这个 snapshot 的 source 仍然是 raw_customers model,configuration 中的 unique key 也没有改变。细微但重要的差异在最后两个 configurations:我们将 strategy 设置为 check,并将 check_cols config 设置为 all

Note

allcheck_cols config 中的 reserved string,用来告诉 dbt 你希望检查所有 columns 的 row changes。

虽然这种 approach 非常简单,但随着 source 中 columns 数量增加,dbt 为捕获这些 changes 生成的 query 会越来越复杂。从 implementation 角度看,这不是问题;事实上,这是展示 dbt 强大能力的绝佳例子。你所做的只是简单定义 check_col config,而 dbt 会生成一个相当复杂的 query 来捕获这些 changes。dbt 能做到这件事确实很棒,在一切正常时也很方便。但当你不可避免地需要 debug snapshot,并查看 dbt 为你 compile 的 query 时,这种 approach 会带来问题。

理解了如何使用这种 approach 配置 check strategy,以及潜在 pitfalls 后,你可能会问什么时候应该使用 check all columns method。根据我们的经验,如果 use case 满足以下标准,最好使用这种 approach:

  • Source table 中没有可靠 timestamp 可用于监控 updates。如果有,请使用 timestamp strategy。
  • Source table 的 columns 数量较少。
  • 你不预期 columns 数量长期会大幅增加。
  • 你需要监控 source table 中所有 columns 的变化。

如果这些 criteria 都满足,你可能可以使用 check all method。这种 approach 能让事情保持简单,让你更多专注于开发,而不是过度设计 Type-2 dimension。话虽如此,还有另外两种 approach 可以用于在 snapshot 中实现 check strategy。

Check Columns in a List

前一个示例中,我们使用 check strategy 设置 snapshot,并使用 all keyword 告诉 dbt 检查 source 中每个 column 的 row changes。然而,我们并不总是需要检查每个 column 的变化。有时你知道某个 dimension 中的 field 永远不会变化,或者即使某个 field 变化了你也不关心。这两种情况下,明确列出需要检查 changes 的 columns 是合理的。

对于这个示例,可以参考文件:customers_snapshot_check_strategy__list.sql,了解我们如何配置 snapshots。此外,要在 warehouse 中构建这个 snapshot,请运行:

dbt snapshot --select customers_snapshot_check_strategy__list

与刚才的 check all columns method 相比,这个 snapshot 中的 configurations 略有不同。Config block 中所有内容都一样,除了 check_cols config。我们不会把它设置为 all keyword,而是传入一个 string list,对应 source 中的 column names。假设我们只希望 dbt 根据以下 columns 监控 row changes:

  • Name
  • Phone

如果这些是我们唯一关心的 columns,那么只需要将 config block 更新为:

{{
   config(
     target_schema='snapshots',
     unique_key='CustomerId',
     strategy='check',
     check_cols=['Name', 'Phone']
   )
}}

现在运行 snapshot command 时,dbt 只会检查这两个 columns 是否存在差异。如果未来某个时候决定需要将 email column 纳入 config,只需将其添加到 columns list 中:

{{
   config(
     target_schema='snapshots',
     unique_key='CustomerId',
     strategy='check',
     check_cols=['Name', 'Phone', 'Email']
   )
}}

可以想象,控制哪些 columns 用于触发 snapshots 中的新 rows 是很有用的。然而,从 developer 的角度看,这也带来了一些额外 responsibility 和 maintenance。与 check all columns method 相比,这本身就带来了几个问题。

考虑一个场景:source table 新增了一个 column。作为 developer,你需要决定是否希望将这个 new field 包含到 configured list 中。但在做这个决定前,你必须知道 source 新增了这个 column。虽然许多组织中的 data engineers 和 software engineers 正在通过 defined schemas 和 data contracts 努力更紧密合作,但并不是每个组织都达到了这种 maturity level。因此,你需要考虑你的组织如何运作,才能解决这个问题。

这并不是说这个 approach 不好,我们只是希望你意识到实现该 solution 时需要考虑的事项。事实上,在我们看来,这种方法仍然比 checking all columns 更好,因为它减少了 dbt 生成的用于检测 row changes 的 query 的复杂性。选择方法时要有意图,不要只是因为更容易就默认检查所有 columns。

Check a Hash Column

本节最后,我们通过一个示例说明如何使用 hash column 检测 source 中的 row changes。如果必须使用 check strategy,这是我们最喜欢的 snapshots implementation approach,原因有几个:

  1. 你可以利用 dbt 最强大的 features 之一:Jinja。
  2. 可以通过一个 string value 监控每个 column 的变化。
  3. dbt 生成的用于监控 row changes 的 query 会被简化,因为它只比较一个 column。

Note

这个示例中,我们会引用文件:customers_snapshot_check_strategy__hash.sql

在介绍如何使用这种方法设置 snapshot 前,先讨论一下 “hash column” 的含义。在我们的示例语境中,hash column 指的是一个 single string column,它是将 record 中所有 columns concatenate 起来,并对这个 concatenated string 应用 hashing algorithm 后得到的结果。Data warehousing 中常见的 hashing algorithms 包括 MD5 和 SHA-2。

Caution

与更长的 SHA-2 相比,MD5 algorithm 生成的 string 更短,因此 hash collision 的概率更高。只有当你预期 table 中会有极大 record count 时,这才会成为问题。

看一下 Listing 5-4,它展示了如何使用 MD5 algorithm 生成 hash column。

select
  *,
  md5(cast(coalesce(cast(CustomerId as text), null) || '-' ||
  coalesce(cast(Name as Text), null) || '-' ||
  ...
  coalesce(cast(UpdatedAt as Text), null) as text)) as HashDiff
from {{ ref('raw_customers') }}

Listing 5-4:生成 source 中所有 columns 的 hash value 的示例

我们通过将 source 中所有 columns cast 为相同 data type,然后 concatenate 成单个 string 来生成 hash。最后,将 MD5 algorithm 应用于这个 string,得到一个 32-character string value,它对每一行都是 unique 的。表 5-7 展示了结果示例。请记住,我们使用的是 Snowflake,因此你可能需要稍微调整,以适配自己的 SQL dialect。幸运的是,dbt 已经有解决方案,我们会在示例中讨论。

表 5-7:名为 HashDiff 的 hash column 示例

CustomerIdNameHashDiff
1Genevieve Trevino74c6f4ac156965f202c7c732d3c577ab

要开始结合 hash column method 使用 check strategy 实现 snapshot,需要在 snapshot file 中添加一个 transformation 来生成 HashDiff。可以用两种方式做这件事:第一种是像 Listing 5-4 中那样自己生成 hash;第二种是利用 dbt macros 为我们生成这个 column。第 6 章会非常详细地覆盖 macros,但现在是开始揭示 macros 和 Jinja 在简化 code 方面威力的好时机。

如果你查看这个示例文件 customers_snapshot_check_strategy__hash.sql,或者下面的 code snippet,会看到我们使用 Jinja functions 和 macros 在 snapshot 中生成 HashDiff column:

select
  *,
  {{ dbt_utils.generate_surrogate_key(adapter.get_columns_in_relation(ref('raw_customers'))|map(attribute='name')|list) }} as HashDiff
from {{ ref('raw_customers') }}

由于本章不是要细粒度覆盖 Jinja 和 macros,我们先从 high level 理解这个 statement 如何生成 hash column。如果看这个 statement 最内层部分,也就是 Listing 5-5,首先发生的是:我们使用 adapter Jinja wrapper 中的 get_columns_in_relation method。这个 method 会返回一个 object,包含 relation 中 columns 的 metadata。该 method 接收一个 input parameter,也就是 relation,它可以通过 dbt function 传入,用于引用 source 或 upstream model。虽然可以传入 static table name,但和你在 dbt 中写代码的任何其他时候一样,应尽量避免这样做。由于这个 method 提供的信息不只是 column name,我们添加了一点 Jinja 来 map 到 column names,然后将它们转换为 list。

adapter.get_columns_in_relation(ref('raw_customers'))|map(attribute='name')|list

Listing 5-5:从 raw_customers model 获取 column names list

我们这样做,是因为 dbt_utils.generate_surrogate_key macro 期望接收一个代表 column names 的 strings list。随后,来自 dbt_utils package 的 generate_surrogate_key macro 会 concatenate 所有这些 columns,并对它们应用 MD5 algorithm,为我们创建 hash column。这实际上与我们在 Listing 5-4 中手动做的事情相同,但这里利用 Jinja 和 macros 的力量,帮我们承担部分繁重工作,使 code 更 DRY。话虽如此,第 6 章会更详细介绍 packages,例如 dbt_utils

现在,我们的 transformation 中有了一个 column,它会为作为 snapshot source 的 table 中所有 columns 创建 hash。在我们的案例中,这个 source 是 raw_customers model。现在可以更新 config block,使 check_cols configuration 只查看 HashDiff column 来监控 row changes:

{{
   config(
     target_schema='snapshots',
     unique_key='CustomerId',
     strategy='check',
     check_cols=['HashDiff']
   )
}}

那么,既然 dbt 已经在 check strategy 中内置了 all keyword,为什么还要费这么大劲?这种 solution 比 check all 更好有几个原因:

  • dbt 生成的 query 会更简单,因为它只检查一个 column 的 changes,但你仍然获得跟踪部分或全部 columns 变化的收益。
  • 与 check all columns method 相比,这种方法可能获得更好的 performance。这里我们谨慎使用“可能”,因为影响 performance 的因素太多。

如果需要使用 check method 实现 snapshot,我们建议你始终采用这种 approach。它不仅带来前面提到的好处,还能标准化你使用 check method 实现 snapshots 的方式。你甚至可以将其 templated,这样通过替换 source references,几分钟内就能启动一个新的 snapshot。

Additional Configurations

虽然 snapshots 有几个 required configurations,但也可以添加其他 configurations 来改变 dbt snapshots 的 behavior。本节中,我们会讨论如何配置 snapshot behavior,以处理 source 中的 hard deletes,以及如何在指定 database 中构建 snapshot。

虽然我们会覆盖这些 snapshot-specific configs,但还有几个其他 configs 你会从 models、seeds 等资源中感到熟悉。如果需要,也可以设置以下 optional configurations:

  • enabled
  • tags
  • pre-hook
  • post-hook
  • persist_docs
  • grants

Hard Deletes

在前面的示例中,我们没有处理使用 Type-2 SCDs 时经常出现的 hard deletes 问题。Snapshots 有一个 optional configuration 可以设置,它会监控 source 中 deleted records,并在 dimension table 中将它们设置为 inactive。

Note

dbt 只会监控 hard deletes。如果你的 source table 使用 soft delete method,你需要在 downstream model 中处理这些情况。

如果你 snapshot 的 source 存在 hard deletes,应考虑启用 invalidate_hard_deletes config,将其设置为 True:

invalidate_hard_deletes=True

dbt 会向 snapshot process 添加几个额外步骤,用于监控并 deactivate deleted records。dbt 执行的第一步是识别 source table 中哪些 rows 已被 deleted。这通过常见的 left anti-join SQL pattern 实现。意思是,dbt 会将 source table left join 到 snapshot table,并检查 source table 中的 nulls。Join 基于你配置的 unique_key 构建。图 5-1 直观展示了 dbt snapshots 如何识别这些 hard deletes。实际上,dbt 查询的是存在于 snapshot 中、但不再存在于 source table 中的 records。

image.png

图 5-1:left anti-join 的视觉表示

一旦 dbt 识别出从 source 中删除的 rows 集合,它会在 snapshot table 中更新这些 rows,将 dbt_valid_to meta field 设置为 snapshot run 时的 timestamp。与 snapshot 的 inserts 和 updates 不同,这个 field 必须使用 current timestamp 设置,而不是 source 中的 timestamp,因为 record 已经不存在了。这个问题可以通过另一种 tracking history in tables 的方式解决,我们会在本章后面讨论 snapshots 的潜在问题时谈到。

Target Database

虽然你的 project 在 profile 中配置了 default database,但有时可能需要将 tables materialize 到另一个 database 中。当你开始使用 environment variables,使 project 能够在不同 environments 中运行时,这非常有用。其他 dbt assets,例如 models 和 seeds,使用 database config;但 snapshots 要实现相同行为,需要使用 target_database config。

Setting Snapshot Configurations

本章示例中,我们一直使用 .sql files 顶部的 config block 配置 snapshots。但与 models 和 seeds 类似,你也可以在 .yml files 中配置 snapshots。这可以通过在 dbt_project.yml 中定义 configs,或者在其他某个 .yml file 的 config resource property 中定义来完成。

Tip

为了保持 project organized,我们设置 <directory>_schema.yml files。对于 snapshots,这对应的是 snapshots_schema.yml

Snapshots 的 configs 应用 hierarchy 遵循图 5-2 中的 pattern。它表示 dbt 会使用最低层级的 configuration。例如,如果你在 dbt_project.yml 和 snapshot 的 .sql file 中都定义了 target_database,那么 .sql file 中设置的 configuration 会优先。

image.png

图 5-2:snapshots 的 configuration hierarchy

Setting Required Configurations

虽然 dbt 会在我们讨论过的 hierarchy 中寻找应该用于 snapshot 的 configurations,但我们想确保你意识到 required snapshot configurations 应该设置在哪里有一个细微点。回忆本章前面,required configs 包括:

  • target_schema
  • strategy
  • unique_key
  • check_cols
  • updated_at

细微点是:你应该只在 snapshot .sql file 顶部的 config block 中,或直接在 dbt_project.yml file 中设置这些 configs。应避免在其他 .yml files 的 config resource property 中设置这些 configs,因为这样设置时它们可能无法工作。原因是 dbt parse project 时,会查找 required configs 是否在 project level 或直接在 snapshot file 中设置;如果这两个地方都没有设置,dbt 会抛出 error。你可以通过始终在 dbt_project.yml 中设置 default value 来绕过这个问题,但这并不理想,而且在 debug 时很可能让你头疼。

How dbt Handles Schema Changes

Data professionals 在处理 raw source data 时,经常会遇到 schema changes。这些 changes 往往发生在 upstream,直接出现在 software engineers 拥有的 application 或 product database 中,或者发生在你正在拉取数据的 API 中。无论 data 来自哪里,都应该理解你的 data pipeline 如何处理 schema changes,这样当 inevitable changes 发生时,你不会手忙脚乱地修复 broken pipeline。

我们说 schema changes 到底是什么意思?我们喜欢把 schema changes 理解为 source data 的 structure 或 format 的任何变化,例如:

  • Changing table names
  • Adding or dropping tables
  • Changing column names
  • Adding or removing columns
  • Data type changes
  • Keys changing

所有这些 schema changes 都很重要,需要在 data pipelines 中理解并预判。但在 snapshots 的上下文中,我们只覆盖其中一些。我们会聚焦 column-level changes,以及 dbt 如何处理 new columns、removing columns 和 existing columns 中的 data type changes。

对于我们将要讨论的任何 changes,dbt 只有在你的 query 中选择了 changed 或 new column 时,才会捕获这些 changes。例如,如果 snapshot query 是类似 select * from {{ some_source }},那么每当 column 被 added、altered 或 removed 时,dbt 都会感知到。你也可以在 query 中列出想 select 的 columns,但如果你选择的某个 column 从 source 中被 removed,就可能遇到 failures;或者如果 source 新增了某个 column,dbt simply won’t know to add it to the snapshot。在选择一种方法而不是另一种方法时存在 trade-offs。例如,当选择 all columns 时:

  • dbt 会更好地处理 schema changes。
  • 但如果 source 有很多 columns,而你并不需要 snapshot 全部 columns,可能会遇到 performance issues。

在判断 snapshot query 中最好 select all 还是 specific columns 时,需要考虑这些 trade-offs。现在深入看看 dbt 如何处理 snapshots 中的 schema changes。

Adding New Columns

你最可能遇到的常见 schema change 之一,是 data source 中新增 columns。这通常发生在 upstream 需要该数据时,或者也可能是你专门为了 analytics use case 请求添加的。无论是哪种情况,你都需要知道 dbt 如何处理 snapshot query 中新增的 columns。

幸运的是,当 dbt 在 snapshot query 中检测到 new column 时,会将这个 new column 添加到 data warehouse 中的 destination snapshot table。看表 5-8:如果 raw customers source file 中新增了一个 DateOfBirth column,并且使用的是 select * from {{ some_source }} statement,它会如何影响 snapshot destination table。

表 5-8:new column 被添加到 snapshot destination table 的示例

CustomerIdNameUpdatedAt
3Leslie Rollins2023-02-24 20:07:47.418
CustomerIdNameUpdatedAtDateOfBirth
3Leslie Rollins2023-02-24 20:07:47.4181989-06-04

可以看到,由于 query 是 select all,dbt 会自动捕获这个 field,并将它 append 到 snapshot destination 的末尾,不需要我们干预。如前所述,如果我们显式列出了要 select 的 columns,那么新的 DateOfBirth field 不会被捕获,除非我们将该 column 添加到 snapshot file 的 select statement 中。

Removing Columns

另一个 schema change 示例是某个 column 从 snapshot query 中被移除。这可能是你从 query 中移除了该 column,也可能是该 column 从 data source 自身中被移除了。无论为什么该 column 不再属于 snapshot query,都需要理解 dbt 如何处理这种 schema change。

当某个 column 从 snapshot query 中被移除时,dbt 会通过不再向 destination table 中该 column 添加 values 的方式处理这个 change。幸运的是,dbt 不会删除该 column,因此你仍然会保留对该 column 已经 tracked 的 history。

Note

即使某个 column 从 source 中移除了,它也不会从 snapshot destination table 中被删除。

表 5-9 展示了如果我们从 snapshot query 中移除 Phone field 会发生什么。在这个示例中,customer 501 是一条 newly added record。可以看到,如果使用 select * from {{ some_source }} statement,Phone field 仍然存在,但对于这个 new customer record 来说是 null。

表 5-9:source 中移除某个 column 时 snapshot destination table behavior 示例

CustomerIdNamePhone
3Leslie Rollins1-584-147-6573
501Lionel Messinull

Data Type Changes

最后一种需要注意的 schema change,是当 snapshot 的 source 中 data types 发生变化时,dbt 会做什么。这与我们前面讨论的其他 changes 有些不同,因为 dbt 会根据 snapshot destination table 中的 data type,以不同方式处理 data type changes。

通常,即使 source 中的 data type 发生变化,dbt 也不会改变 destination 中的 data type。例如,如果某个 column 的 data type 是 date,但 upstream 将 data type 改成 timestamp,dbt 不会尝试在 snapshot destination table 中协调这个 change。因此,所有现在是 timestamps 的 values,在 insert 到 snapshot table 中时都会被转换成 dates。如果你知道 snapshot source 中某个 data type 正在变化,应牢记这一点。为此设置 monitoring 和 alerting 也可能很有用,这样当 raw / source data 中发生这类 changes 时,你能知道。

有一种特殊场景下,dbt 会尝试更新 destination table 中 column 的 data type。如果 destination column 是某种 string type,而 upstream 中该 string column 的 size 增加了,dbt 会在 snapshot table 中增加 string column 的 size。例如,如果有一个 column BusinessName,类型是 varchar(255),而 upstream 中它变成了 varchar(500),那么 dbt 会尝试跟随变化并扩展 column size。

Using Snapshots in Downstream Models

到目前为止,我们用整章内容展示了实现 snapshots 的不同方法。现在切换一下视角,看看如何在 downstream models development 中实际使用 snapshots。让我们看一个示例,展示 snapshots 如何被 query 并用于 downstream models。在这个示例以及本节中,我们会使用 fct_orders.sql file。

开始示例之前,回忆本章前面说过,你几乎总是应该像对待 sources 一样对待 snapshots。也就是说,snapshot 中应该没有 transformations,或只有非常有限的 transformations。如果需要对 data 做一些 light transformations,建议在 staging model 中完成。在那里可以 rename columns、cast data types,或执行其他 light transformations。不过,为了保持示例简单,我们会直接在 downstream model 中引用 snapshot。只需要记住,实践中你可能需要在这两个步骤之间加入 staging model。

Referencing Snapshots

Snapshots 在 downstream 中使用 {{ ref() }} function 引用。到这里你应该已经熟悉了,因为这也是引用 models 和 seeds 的方式。在我们的示例中,我们开始构建一个 fact table,它由两个 assets 组成:

  1. models/staging/furniture_mart/stg_furniture_mart_orders.sql
  2. snapshots/customers_snapshot_timestamp_strategy.sql

这是一个简单 transformation,将这两张 tables join 在一起,把 order-related information 和 customer name 纳入 model。如下面示例所示,尽管一个是 model,另一个是 snapshot,这两个 dbt assets 都使用 {{ ref() }} function 引用:

select
 ord.OrderId,
 ord.CustomerId,
 cus.Name,
 ord.SalesPerson,
 ord.OrderStatus,
 ord.OrderPlacedTimestamp
from {{ ref('stg_furniture_mart_orders') }} as ord
left join {{ ref('customers_snapshot_timestamp_strategy') }} as cus
 on ord.CustomerId = cus.CustomerId

如果你想查看目前这个 query 会产生什么结果,可以运行以下 command,让 dbt build 这个 model:

dbt run --select fct_orders

不过,一旦它在 data warehouse 中成功 build,你接下来应该 query 这张 table 的结果,检查 table grain,确保 transformation 按预期行为运行。可以通过检查 destination table 中 orders 数量是否等于 distinct orders 数量来完成,因为我们期望这张 table 每个 order 有一行。运行以下 query:

select
  count(OrderId) as NumberOfRecords,
  count(distinct OrderId) as DistinctNumberOfOrders
from dbt_learning.public_operations.fct_orders

检查这个 query 后,你会注意到这张 table 中有 1005 条 records,但只有 1000 个 distinct orders。因此 transformation 某处存在问题。这个 join fanout 是由 snapshot table 导致的。记住,snapshot tables 会跟踪 history,因此对于被跟踪的 unique id,每个 id 可能有多条 records。在这个案例中,snapshot table 跟踪 Customers 的 changes,因此 table 中每个 customer 可能有多条 records。回忆本章前面,我们对 CustomerIds 1 和 30 做过 updates。幸运的是,在处理 Type-2 dimensions 时,使用 snapshots downstream models 时,有几种方式可以解决这个问题:

  1. 只查询 active snapshot records。
  2. 查询某个 point in time 的 snapshot record。

Query for the Active Record

引用 snapshots 且不必担心 join fanout 的最直接方式,是只查询 active record。回忆 dbt 第一次运行 snapshot 时会始终添加到 snapshot tables 中的 meta fields。其中一个特定 meta field,也就是 dbt_valid_to,会帮助我们只检索 active row。

让我们更新前一节的示例,添加 where clause,将返回 records 限制为 dbt_valid_to column 中 value 为 null 的 records,因为这表示这条 record 是 active record。修改后,fct_orders model 中的 query 应该是这样:

select
 ord.OrderId,
 ord.CustomerId,
 cus.Name,
 ord.SalesPerson,
 ord.OrderStatus,
 ord.OrderPlacedTimestamp
from {{ ref('stg_furniture_mart_orders') }} as ord
left join {{ ref('customers_snapshot_timestamp_strategy') }} as cus
 on ord.CustomerId = cus.CustomerId
where cus.dbt_valid_to is null

然后像之前一样 run 并 validate results。注意,从 validation query 可以看到,table 中 records 数量确实等于 distinct orders 数量,即 1000。这解决了 join fanout issue。但这种方法并不会提供基于 order placed 时刻的 customer data。它告诉你的是 snapshot table 中 customer record 的 current state。根据 use case,这可能没问题,甚至是理想方式。但接下来看看如果想在某个 point in time 查询 customer snapshot table,应如何组织 transformation。

Caution

如果你必须处理 sources 中的 hard deletes,并且配置了 invalidate_hard_deletes=True,使用 active row method 时要小心,因为可能没有 active record 可返回。因此,与 hard-deleted customers 关联的 orders 不会被返回。

Query for a Record at a Point in Time

如果你需要在 dbt transformation 中返回某个 point in time 的 snapshot record,可以使用 snapshot meta fields 做 time range join。只要 performance 可接受,这是我们 preferred method,因为相比 active record method,它能让 models 更 idempotent。原因是,每次使用 point-in-time method 运行 downstream model 时,对任何给定 record 都会返回相同 result。然而,active record method 不会始终返回相同 result。事实上,每当 snapshot 中对应 record 被 updated 时,它都会变化。

要使用 point-in-time method,需要移除前一个示例中添加的 where clause,改为更新 join criteria。你应该更新 join criteria,使其还基于一个 timestamp 进行 join,该 timestamp 位于 dbt_valid_fromdbt_valid_to 之间。你在这个 criteria 中使用的 timestamp 应该与 transformation 的上下文匹配。在本示例中,我们使用 OrderPlacedTimestamp 返回 order placed 时的 customer record:

select
 ord.OrderId,
 ord.CustomerId,
 cus.Name,
 ord.SalesPerson,
 ord.OrderStatus,
 ord.OrderPlacedTimestamp
from {{ ref('stg_furniture_mart_orders') }} as ord
left join {{ ref('customers_snapshot_timestamp_strategy') }} as cus
 on ord.CustomerId = cus.CustomerId
 and ord.OrderPlacedTimestamp between cus.dbt_valid_from and ifnull(cus.dbt_valid_to, '2099-12-31'::timestamp_ntz)

在验证这些 results 之前,也注意 join criteria 中,我们检查 dbt_valid_to column 是否为 null,并在这种情况下填入一个任意的未来日期作为 default value。必须这样做,因为 active record 也可能正是正确的 point-in-time record。最后,像前两个示例一样,可以运行相同 validation query,并看到这种方法也解决了 fanout issue。

Potential Issues with Snapshots

回忆本章前面 “When to Use Snapshots” section 下的 Example One。在这个示例中,我们讨论了如何实现 dbt snapshot,以便在 data warehouse 中跟踪来自 PostgreSQL 或 SQL Server 等 relational database 的 data source 的 row changes。

可以看到,实现 dbt snapshot 可以很好地帮助我们保留这些 changes 的 history。不过,在确定如何跟踪像 orders table 这样的 data source history 时,我们希望你意识到使用 snapshots 有两个 caveats。首先,当使用 dbt snapshot 某个 data source 时,需要理解 data 被 generated、updated 和 deleted 的 velocity,因为这些因素会直接影响第二个 caveat:snapshot frequency。使用 snapshots 时必须同时考虑这两个因素,因为如果 data source 的 velocity 很高,但你 snapshot 的 frequency 不够高,就可能存在 missing data 的风险。

将表 5-10 看作 OrderId 21,也就是 Listing 5-1 中那个 order 的完整 history 的假设示例,我们可以用它分析 status 被 updated 的 velocity,以及 snapshot frequency 会如何影响 data warehouse 中 history tracking 的状态。这个示例相当直接:该 order 最初在 2020 年 3 月 6 日 placed,随后经历了四次 updates,其中 OrderStatus 变化了几次。这里发生了一个有趣情况:OrderStatus 在 shipped 和 placed 之间来回切换了几次。

表 5-10:OrderId 21 的完整 history 示例

OrderIdOrderStatusOrderPlacedTimestampUpdatedAt
21placed2020-03-06 08:26:31.0002020-03-06 08:26:31.000
21shipped2020-03-06 08:26:31.0002020-03-07 22:51:34.000
21placed2020-03-06 08:26:31.0002020-03-07 22:52:00.000
21shipped2020-03-06 08:26:31.0002020-03-10 07:54:01.000
21returned2020-03-06 08:26:31.0002020-03-19 19:27:41.004

在这个示例中,假设发生了一个 operational error:某位 employee 在 2020 年 3 月 7 日错误地将该 order 标记为 shipped,然后几乎立刻又改回 placed。尽管这是一次错误操作,但在 data warehouse 中跟踪它仍然很有价值。

现在我们已经理解该 record 如何随时间 updated,接下来考虑对这张 table 运行 dbt snapshot command 的 frequency。通常,snapshots 会每天一次,甚至每小时一次,对 source tables 运行。我们需要确定正确 frequency,或者采用 alternative approach,以防止 data loss。

假设我们每小时整点运行一次 dbt snapshot command。即使有这个 frequency,最终也无法跟踪这条 record 的完整 history,而是会得到类似表 5-11 中的 records。注意,与表 5-10 相比,我们丢失了 OrderStatus 被错误 updated 为 shipped 的那条 record,但捕获到了对该错误的 correction。为什么会发生这种情况?

表 5-11:来自 snapshot 的 OrderId 21 history,其中存在 missing data

OrderIdOrderStatusOrderPlacedTimestampUpdatedAt
21placed2020-03-06 08:26:31.0002020-03-06 08:26:31.000
21placed2020-03-06 08:26:31.0002020-03-07 22:52:00.000
21shipped2020-03-06 08:26:31.0002020-03-10 07:54:01.000
21returned2020-03-06 08:26:31.0002020-03-19 19:27:41.004

发生这种情况的原因是:由于我们在整点进行 snapshot,只会捕获 source 中 record 的 current state。注意,2020 年 3 月 7 日 22 点这个小时内,order 21 经历了两次 updates。dbt snapshots 没有办法跟踪 intermittent changes,因此我们没有任何关于那次 accidental update 的 history,也就是 employee 错误地将该 order 标记为 shipped 的那次变化。

解决这个问题有几种方式,最简单的 solution 是提高 snapshot source table 的 frequency。不过,为了捕获这类 changes,你几乎需要每分钟甚至更频繁地 snapshot source table。因此,如果你需要跟踪高 velocity source data 的 changes,探索其他选项可能更有利,例如 transaction log–based change data capture 或 event streams。

Summary

本章中,我们介绍了 dbt snapshots,以及什么时候在实践中使用它们是合理的。我们通过两个示例展示了当 sources 具有不同 volume 和 velocity levels 时,使用 snapshots 可能是什么样子。这让你对 snapshots 的用途,以及什么时候最适合使用它们,有了基础理解。

随后,我们讨论了如何通过 dbt 添加的 meta fields,识别 snapshot records 什么时候被 updated,或者如何识别 active record。这些 fields 是 Type-2 slowly changing dimensions 中常见的字段,幸运的是,dbt 会自动将它们添加到 snapshots 中。请记住,dbt 会添加四个不同 meta fields,但实践中你主要会使用 dbt_valid_fromdbt_valid_to fields。

接着,我们讨论了可用于实现 snapshots 的不同 strategies。首先覆盖的是 timestamp strategy,它使用 source table 中可信 timestamp,在 snapshot destination table 中捕获 updates。这个 strategy 是实现 snapshots 最简单、最有效的方式,因此除非你没有可用于判断 updates 的 timestamp,否则它应该始终是默认选择。当没有可用于 snapshots 的 timestamp 时,可以使用 check columns strategy。实现这个 strategy 有多种不同 approach。你选择哪种方法,应受 data volume、data velocity,以及被 snapshot 的 source table 中 columns 数量的影响。

一旦实现 snapshot,就必须配置它,使 dbt 知道如何在 warehouse 中构建它。这通过一组 required configurations 完成,但也可以设置 optional configurations。其中影响最大的 optional configuration 是 invalidate_hard_deletes

从 upstream sources snapshot data 往往带有某种不确定性,因为 analytics engineers 和 data engineers 通常并不拥有这些 upstream sources。因此,这些 sources 的 schemas 经常在很少沟通,甚至没有沟通的情况下变化。正因如此,snapshots 需要对 upstream schema changes 具备 resilience,因此我们覆盖了 dbt 如何处理 snapshots 中的 schema changes。

到这里为止,我们只覆盖了如何实现 snapshots。但如果不讨论如何在 downstream 使用它们,所有这些努力都会被浪费。因此,我们覆盖了一个将 snapshot 纳入 downstream model 的示例。在该示例中,我们展示了如何引用 snapshots,以及如何查询 active record 或某个 point in time 的 record。

虽然 snapshots 是 dbt 中非常有用的工具,但它们并非没有 pitfalls。因此,在本章最后一节,我们覆盖了 snapshots 的潜在问题。Snapshots 的主要问题是:它们只在运行时捕获 record。因此,如果某条 record 自上次 snapshot 运行以来已经 updated 两次,那么只有最近一次 change 会被捕获。根据 data velocity,这可能不是问题;但如果它是问题,我们也分享了另一类 history tracking,例如 log-based replication,可能更值得探索。

我们希望本章让你了解了什么时候应该使用、什么时候不应该使用 snapshots,以及如何在其他 dbt models 中实现、配置和引用它们。下一章中,我们将介绍如何使用 Jinja 和 macros,将 dbt transformations 提升到下一层。