在并不遥远的过去,大多数 data teams 完全依赖围绕 drag-and-drop graphical user interfaces(GUIs)或 stored procedures 构建的数据转换工具。遗憾的是,这类解决方案的 implementations 往往最终变得非常复杂,并且长期处于不可靠状态。主要原因之一是,software engineering best practices 没有被使用,而这些实践同样适用于 data teams。例如,data transformations 往往不够 modular,data quality testing 很少被实现,version control 也没有被使用。
除了这些问题之外,使用 stored procedures 或内部自建 transformation tool 的团队,还需要自己编写 boilerplate code 来管理 data warehouse 中的 objects。例如,你需要编写 DDL 来创建 table,并编写 DML 在 transformation pipeline 后续运行时更新该 table。幸运的是,dbt 抽象掉了这些 boilerplate code,并赋能你弥补其他 transformation tools 和 processes 在 software engineering best practices 方面的缺陷。
在本章中,我们将讨论可以使用 dbt 实现的不同类型 models。截至本书写作时,dbt 提供了使用 SQL 和 Python 编写 models 的能力。然后,我们会查看 SQL 和 Python models 都可用的 materializations 类型、model configurations,以及可用于运行 models 的 commands。在本章前几个 sections 中,我们会通过示例带你构建 models,并使用 dbt projects 中常见的 staging-intermediate-marts pattern。不过,正如我们反复提到的,dbt 只是一个帮助你完成 data transformations 的工具,至于如何组织这些 transformations,完全取决于你。在这些示例中,你主要应该关注 syntax,而不必太纠结 project structure。在本章后面,我们会讨论 transformations 中的 modularity,并在该部分介绍几种可能的 project structuring 方式。
SQL Models
在我们看来,构建 models 是你应该理解的 dbt 中最重要的部分。对 dbt 中 SQL models 的扎实理解,将为本书剩余内容奠定基础,因为后续每一章都会建立在这些知识之上。例如,在第 6 章中,我们会讨论 Jinja 和 macros,它们经常用于在 SQL models 的 compilation step 中加入类似 Python 的 control flows。又比如第 8 章,我们会讨论 tests,以及如何实现 tests 来检查 models output data 的质量。
如今,dbt 允许 users 同时使用 SQL 和 Python 编写 models,但总体而言,SQL 仍然在 dbt 和整个 data industry 中占据主导地位,是 data transformations 的事实标准语言。本章后面会讨论 dbt 为什么增加 Python 支持,以及什么时候用 Python 构建 models 更合理。但现在先聚焦 SQL。截至本书写作时,我们坚定认为,对大多数人来说,使用 SQL 编写 models 应该是学习 dbt models 的起点。我们这样认为有几个原因,包括:
- 所有 dbt adapters 都支持 SQL models。
- 对简单 transformations 来说,SQL models 往往是更好的选择。
- 入门门槛极低,因为一切都是 select statement。
- SQL 在 data professionals 中的历史采用度比 Python 更广。
简而言之,我们认为,对大多数人来说,构建 dbt models 时 SQL 应该是默认首选语言;只有当 transformation 很复杂,并且可以用 Python syntax 更优雅地处理时,才应该使用 Python 构建 models。本章后面会讨论一些这样的 use cases。现在先讨论 dbt 中 SQL models 可用的 materialization types。
回忆第 1 章中我们介绍过 dbt 中 materializations 的概念。Materialization 可以定义为:dbt 用来生成 models,并将它们写入或不写入 database 的方法。SQL models 可以使用四种 materializations:
- View
- Table
- Incremental
- Ephemeral
本节剩余部分中,我们会讨论选择 materialization 时应考虑的因素,并通过示例展示如何实现每一种 materialization。如果你想跟着这些示例操作,请确保已经完成第 2 章中讨论的 project setup process,并且已经使用本书 GitHub repository 中包含的 .csv files 正确 seed 了 database:
github.com/apress/unlo…
如果你还需要 seed database,只要已经正确设置 dbt project,并 checkout 了本书的 git repo,那么只需要执行 dbt seed command。运行该 command 后,你的 database 中会出现四张 tables,本书剩余部分会使用它们构建 transformations:
raw_productsraw_ordersraw_orderitemsraw_customers
View(以及 Model Basics)
我们首先讨论的 materialization 是 view materialization。刚开始使用 dbt 时,对大多数有 SQL 背景的人来说,view materialization 最容易理解,因为在 dbt 中写 view 与在 raw SQL 中写 view 非常相似。事实上,这种 materialization 会根据你编写的 SQL select statement,在 database 中为你生成一个 view。稍后我们会通过一个示例带你构建 dbt 中的 view,并说明它与 raw SQL 中写 view 有什么不同。这个示例也会更广泛地解释 dbt models 与编写 vanilla SQL 有何不同。不过,先来看什么时候可能想使用 view materialization。
Caution
不要将 view materialization 与 materialized views 混淆。dbt 中的 view materialization 只是创建 database 中的 standard view object;而 materialized views 是会持久化 data 的 database objects。本书不会讨论 materialized views,因为 dbt 当前并不支持这种 database object 类型。
你可能知道,view 是存储在 database 中、可被查询的 query。View 也可以被看作 virtual table,因为 view object 内部并不持久化任何 data。因此,每当有人查询 view 时,database 都必须运行完整的 underlying query。可以想象,如果你正在处理复杂 transformation,这可能会引入 performance issues。表 4-1 列出了使用 view materialization 的优点和缺点。
表 4-1:View materialization 的优点和缺点
| Advantages | Disadvantages |
|---|---|
| 创建简单 | Performance 可能快速下降 |
| 在 database 中占用 minimal storage | 如果 source table(s) 有不可预测的 schema changes,可能难以维护 |
| 最新 data 始终可用 | 不适合 complex transformations |
考虑表 4-1 中的优缺点,我们认为你应该有意识地使用 view materialization,而不是简单把它作为默认选择。这与 dbt 官方 documentation 中的建议有些不同,官方通常建议一开始使用这种 materialization 构建 models,然后在需要 performance improvements 时再转向更复杂的 materialization。这个建议对刚开始构建 dbt models 时是合理的,但当你更有经验,并熟悉自己 database 的 optimization patterns 后,这个建议就有些过度简化了。我们的观点是:当你开始构建一个新 model 时,如果已经知道 view 无法提供足够 performance,那么就不应该从 view materialization 开始,而应该查看 dbt 的其他 materialization options,例如 table materialization。虽然我们不同意 view materialization 应始终作为默认选择,但如果你认为使用 view materialization 不会导致 performance 下降,它也可能是一个不错的起点。不要在 development 早期陷入 overoptimization 的陷阱。保持简单,之后你会感谢自己,也许你的同事也会感谢你。
现在,让我们演示如何使用 view materialization 构建 dbt model。在这个示例中,我们会基于 seed database 中的 orders data 构建一个 model。先看如果用传统方式,也就是 raw SQL 和 DDL(data definition language)boilerplate code,如何构建这个 view。这会作为我们的 baseline:在 database 中构建一个 view。完成后,再看需要做哪些修改,才能将其转换为 dbt model。
Listing 4-1 提供了传统方式创建 view 所需编写的 SQL。在这个示例中,使用 create or replace view as statement 创建 view,使该 view 可以作为 select statement 的结果生成。这个 select statement 很简单,没有任何 joins,只对部分 columns 执行了一些非常轻量的 transformations,例如将 timestamps cast 为不同 formats,以及重新格式化人名。
create or replace view dbt_learning.public.stg_furniture_mart_orders as
select
ord.OrderId,
ord.CustomerId,
initcap(ord.SalesPerson) as SalesPerson,
cast(ord.OrderPlacedTimestamp as timestamp_ntz) as OrderPlacedTimestamp,
ord.OrderStatus,
cast(ord.UpdatedAt as timestamp_ntz) as UpdatedAt
from dbt_learning.public.raw_orders as ord
Listing 4-1:使用 raw SQL 创建 view 的示例
对 Listing 4-1 中的代码做少量修改后,可以很容易将其转换成一个 materialized as view 的 dbt model,如 Listing 4-2 所示。
{{
config(
materialized='view'
)
}}
select
ord.OrderId,
ord.CustomerId,
initcap(ord.SalesPerson) as SalesPerson,
cast(ord.OrderPlacedTimestamp as timestamp_ntz) as OrderPlacedTimestamp,
ord.OrderStatus,
cast(ord.UpdatedAt as timestamp_ntz) as UpdatedAt
from {{ ref('raw_orders') }} as ord
Listing 4-2:配置 view model 的示例
如果比较 Listing 4-1 和 Listing 4-2,你可能会立刻注意到 dbt model 中有三点明显不同:
- 文件顶部有一些 configuration code。
- 没有
create or replace view asstatement。 - 它不是直接从 table select,而是从
{{ ref('raw_orders') }}select。
从文件顶部开始,你可能注意到了一小段 configuration code:
{{
config(
materialized='view'
)
}}
这被称为 config block,用于在 dbt project 的 .sql files 中为 individual model 定义 configurations。可以看到,我们将一个 configuration,也就是 materialized,设置为 'view'。你可能已经猜到,这就是我们指示 dbt 使用该 file 中的 select statement 在 database 中生成 view 的方式。事实上,这也解释了为什么我们不需要在 SQL file 中包含 create or replace view as statement。由于我们已经将该 model 配置为 view,dbt 会在 runtime 为我们注入 boilerplate DDL。因此,开始构建 models 时,你只需要为任何 SQL models 编写 select statements,设置 materialized configuration,剩下的交给 dbt 处理。
Tip
View materialization 是 dbt 的默认 materialization,因此严格来说,你其实不需要像前面那样设置 configuration。不过,我们在这里这样做,是为了展示 configuration 如何使用。并且在我们看来,写代码时明确一点总是没有坏处。
你可能注意到 Listing 4-1 和 Listing 4-2 的最后一个区别是,原始示例中直接引用了 raw_orders database object:
select
...
from dbt_learning.public.raw_orders as ord
但在后一个示例,也就是 Listing 4-2 中,我们使用 ref Jinja function 来引用当前 model 所 select 的 database object。第 3 章中,我们介绍了 source Jinja function,它用于直接引用 database 中未经转换的 objects,也就是 sources。ref function 的工作方式类似,但它不是引用 source database object,而是用于引用 upstream dbt transformation,包括:
- Other models
- Snapshots
- Seeds
如果你还记得本章开头,我们要求你使用 dbt seed command seed database,让本书 GitHub repository 中包含的 .csv files materialized as target database 中的 tables。由于我们创建了这些 seeds,就能够在 downstream models 中使用 ref function 引用它们,例如 Listing 4-2 中的 model。当然,即使你没有使用提供的代码和 seed data,只要你的 project 中有 seed data 或 sources 可供引用,这个逻辑同样成立。接下来注意我们在 ref function 中使用的 string:
from {{ ref('raw_orders') }} as ord
由于我们引用的是名为 raw_orders.csv 的 seed file,dbt 允许我们直接基于 file name 引用它。现在我们只是直接使用 ref function 处理 seeds,但当你开始基于其他 upstream models 的 output 构建 models 时,同样的逻辑也成立。例如,如果有一个 model file 名为 my_first_dbt_model.sql,你想在文件名为 my_second_dbt_model 的 downstream model 中引用它,也会使用 ref function:
{{ ref('my_first_dbt_model') }}
你需要记住,默认情况下 dbt 允许你以这种方式引用 upstream models、seeds 和 snapshots,因为使用 dbt 开发时你会大量这样做。希望这能帮助你理解如何使用 ref function,但为什么要使用它呢?如果你还记得第 3 章,我们讨论过使用 source function 后,dbt 能够推断 dbt assets 之间的 relationships。使用 ref function 也是同样道理,dbt 能够施展它的 “magic”,知道应该以什么顺序构建 models、seeds 和 snapshots。此外,使用 ref function 还为 dbt 提供了生成 DAG 所需的 context,而这个 DAG 可以在 documentation 中可视化。如果你 hardcode database object names,而不是使用 ref 和 source Jinja functions,那么 dbt 就无法感知 assets 之间的 relationships。因此,我们建议永远不要在 transformation code 中直接 hardcode database object names。
虽然下面内容与 view materialization 没有直接关系,但我们希望在本节最后补充几个 model basics,帮助你进一步理解 dbt 如何读取 .sql files 中定义的 models、compile 它们,并运行它们。首先理解 dbt 如何将包含 configurations 和 Jinja functions 的 model files 中的代码转换成 database 能理解的 raw SQL。dbt 的第一步是 compile code,在这一步中,config block 会从 compiled SQL 中移除,ref 或 source Jinja functions 也会被移除并替换为 database objects。Ephemeral models 是一个例外,稍后会讨论,但现在不用担心它。
为了剖析 dbt 在幕后做了什么,可以运行:
dbt run --select stg_furniture_mart_orders
然后查看生成的 compiled 和 run files。先快速说明这个 command 做了什么:它告诉 dbt,你想运行文件:
~/models/staging/furniture_mart/stg_furniture_mart_orders.sql
中的 model。dbt run command 用于运行 models,因此 dbt 会搜索 models directory 及其 subdirectories,查找名称匹配 stg_furniture_mart_orders 的 file。请记住,file names 需要 globally unique,因为否则 dbt 无法自动推断 model names。因此,如果 file names 不唯一,就会遇到 errors。
运行前面的 command 后,可以导航到 dbt project 中的 target directory。target directory 用于存储 dbt 的 run artifacts。第 1 章中讨论过这个 directory 的用途。简单提醒一下,它会在每次运行 command 时由 dbt 自动生成。target directory 用于存储大量 metadata、compiled SQL 和 run SQL,也就是实际发送到 database 的代码。关于该 directory 中每个 artifact 的更多细节,可以回看第 1 章。本书中我们会查看该 directory 中的不同 files,但现在先聚焦于 compiled 和 run subdirectories 中的 files。如果查看 compiled subdirectory,应该会看到一个名为 stg_furniture_mart_orders.sql 的 file。如你所料,该 file 存放的是 model file 生成的 compiled SQL。这直接对应于我们在 models directory 中定义 transformation 的 file name。当然,你可以给这个 transformation 起任何名字,这只是我们在示例中使用的名字。看 Listing 4-3,了解 dbt 如何将 model file 转换成 plain SQL。
select
ord.OrderId,
ord.CustomerId,
initcap(ord.SalesPerson) as SalesPerson,
cast(ord.OrderPlacedTimestamp as timestamp_ntz) as OrderPlacedTimestamp,
ord.OrderStatus,
cast(ord.UpdatedAt as timestamp_ntz) as UpdatedAt
from dbt_learning.public.raw_orders as ord
Listing 4-3:compiled dbt SQL code 示例
这是一个非常简单的示例,但可以看到,config block 已从文件顶部移除,ref function 实际上已经转换为 fully qualified database object name。虽然这很有意思,但还没有展示完整图景,因为它仍然只是一个 select statement,而我们指示 dbt 在 database 中生成一个 view。你可能会想:dbt 如何处理 config block?这个 select statement 又如何创建 view?我们预期 select statement 前面还应该有一些 DDL,例如 create statement。现在,如果我们把注意力从 compiled subdirectory 转向 run subdirectory,应该会再次找到一个名为 stg_furniture_mart_orders.sql 的 file,或者你为 model 选择的名字。
如果打开这个 file,如 Listing 4-4 所示,就会看到所有内容如何组合在一起。注意,compiled SQL query 被注入到一些 DDL 中,用于 create 或 replace 该 view。正如前面讨论的,这之所以发生,是因为我们将 materialization configuration 设置为 'view'。
create or replace view dbt_learning.public.stg_furniture_mart_orders
as (
select
ord.OrderId,
ord.CustomerId,
initcap(ord.SalesPerson) as SalesPerson,
cast(ord.OrderPlacedTimestamp as timestamp_ntz) as OrderPlacedTimestamp,
ord.OrderStatus,
cast(ord.UpdatedAt as timestamp_ntz) as UpdatedAt
from dbt_learning.public.raw_orders as ord
);
Listing 4-4:run dbt SQL code 示例
虽然这个示例非常简单,但可以想象,dbt 在构建 data transformations 时能够节省时间和精力,因为它会替你管理大部分 boilerplate code。随着本章推进,我们会看到更复杂的示例,但在学习基础知识时,我们不想一上来就把你扔进深水区。我们可以总结一下,当运行 dbt model 时,幕后发生的事情如下:
- Model files 被 compile 并转换为 plain SQL。
- Compiled SQL 被注入到适当的 boilerplate code 中,在这个案例中,dbt 添加了创建 view 的代码。
- 包含 boilerplate code 的 compiled SQL,也就是存储在
runfolder 中的代码,会被发送出去,在 data warehouse 中构建 object。
这个过程不仅发生在 view materialization 中,也适用于所有其他 materialization types。现在来看 table materialization 如何工作,以及什么时候可能选择它而不是 view materialization。
Table
很多时候,我们在 data transformation process 中使用 view,或者使用 view 向 end users 暴露 query,随着 view 背后的 data 增长,或 view 中的 logic 变得极其复杂,performance 会随时间下降。也许你也遇到过类似场景。本节中,我们会讨论 table materialization。当 view materialization 开始表现不佳时,table materialization 是 dbt materializations 中下一个合乎逻辑的选择。
顾名思义,这种 materialization 会基于你在 model file 中编写的 select statement,在 database 中构建一张 table,并且每次你针对该 model invoke dbt 时,它都会 create 或 replace 该 table。由于这种 materialization 会在 database 中创建 table,因此与 view 相比,从其中 retrieval data 时通常会获得更好的 performance。这是因为 tables 实际存储 data,而不是像 views 那样只存储引用。
Note
我们会经常提到 “invoking dbt” 或 “executing dbt”。这里的意思是提交一个 command,用于运行一个或多个 models、seeds 或 snapshots。这类 commands 的例子包括 dbt run、dbt build、dbt snapshot 和 dbt seed。
虽然这里不会深入 database tables 的工作细节,但我们认为值得说明 row stores 和 column stores 的区别,以及它们对 analytics workloads 的影响。顾名思义,row store table 会按 rows 存储 data blocks。Row store tables 常见于 Online Transaction Processing(OLTP)databases,因为这类 tables 能够快速 retrieve 并操作单行 data。这对 transaction-based systems 很有用,因为它们通常处理单行或少量 rows,并且 transaction 需要访问它正在操作的整行 data,以便适当 lock record 并在不冲突的情况下进行 updates。然而,row-based tables 对 analytics workloads 的 performance 可能不理想,因为它需要返回被选择的每一行中的所有 data。
幸运的是,许多为 analytics 构建的现代 data platforms,也就是 Online Analytical Processing(OLAP)databases,并不使用 row store tables,而是使用 column store tables。在 column store table 中,data 按 columns 而不是 rows 存储。这支持所谓的 column compression。通过 column compression,data 存储在 individual columns 中,因此每个 storage block 共享相同 data type,storage 可以针对 individual column retrieval 进行优化。Columnar store 对 analytics systems 很有益,因为 data lookups 会快得多,但代价是 data inserts 和 updates 变慢。不过,在 analytics 世界中,更快的 column retrieval 很有优势,因为常见模式是只 select 少数 columns,并对它们运行 aggregation。如果你想更深入了解 row stores 和 column stores,我们建议阅读 Reis J 和 Housley M 的 Fundamentals of Data Engineering(2022)。
现在来看如何使用 table materialization 构建 dbt model。该示例可以在本书 GitHub repository 中找到,路径是:
~/models/intermediate/operations/int_orders_items_products_joined.sql
一如既往,如果想跟着操作,可以使用提供的 repo;如果不想也没关系。我们讨论的概念可以迁移,并容易应用到你自己构建的 dbt models 中。
Listing 4-5 提供了一个 model 示例,在其中我们通过一个 intermediate step 将两个 staging models join 在一起。虽然在我们的 project 中这两个 objects 都没有太多 data,但想象一个真实世界场景,这些 tables 含有数十亿 records。如果是这样,将这两个 joined models materialize as table 可能很有用,这样 downstream 可以只 join 这张 table,而不是每次都访问一个需要重新运行该 join 的 view。
{{
config(
materialized = 'table'
)
}}
select
itm.OrderItemsId,
itm.OrderId,
pro.Product,
pro.Department,
pro.Price
from {{ ref('stg_furniture_mart_orderitems') }} as itm
join {{ ref('stg_furniture_mart_products') }} as pro
on itm.ProductId = pro.ProductId
Listing 4-5:将 model materialize as table 的示例
如 Listing 4-5 所示,materialization configuration 仍然使用 model file 顶部的 config block 设置,这与配置 view model 完全一样,只是这次将 configuration 设置为 'table'。除此之外,只需要编写一个 select statement 来返回应该 materialize 到 destination table 中的数据。如果回忆前一节,执行 model 时主要发生两件事:query 被 compile,然后添加 boilerplate code,使 object 能在 database 中构建。Compile 的概念很直接,所以我们直接看 dbt 将生成并发送到 database 的 SQL。
注意 Listing 4-6 第一行中,dbt 注入了一些 DDL,告诉 database 使用 select query create or replace table。更具体地说,dbt 会将 compiled select statement 包在 create or replace table statement 中。
create or replace table dbt_learning.public.int_orders_items_products_joined as
(
select
itm.OrderItemsId,
itm.OrderId,
pro.Product,
pro.Department,
pro.Price
from public_staging.stg_furniture_mart_orderitems as itm
join public_staging.stg_furniture_mart_products as pro
on itm.ProductId = pro.ProductId
);
Listing 4-6:使用 table materialization 时,dbt 会在 database 中执行的 SQL 示例
还要注意,dbt 创建的是一个 fully qualified database object:
dbt_learning.public.int_orders_items_products_joined
当然,table 名称直接对应于存储该 model 的 .sql file 名称,但也要注意,它选择将 table 放在 dbt_learning database 和 public schema 中。这是因为我们在设置 dbt project 时定义了 target database。我们告诉 dbt 始终在该 database 和 schema 中 materialize objects。本章后面会进一步讨论 model configuration,以及如何修改这一行为。
本节前面提到过,但值得再次说明:每次针对对应 model invoke dbt 时,table materialization 都会在 database 中重建整张 table。虽然这对 end users 来说通常比 view performance 更好,但如果处理大量 data,每天重建 tables 可能并不现实,因为 processing data 会花很长时间。下一节中,我们会查看一种 materialization,通过 incrementally loading data into destination tables 来解决这个问题。
Incremental
到目前为止,我们覆盖的两种 materialization types:table 和 view,都是每次运行 dbt 时重新创建 database 中的 objects。对于转换低数据量的 teams,或者拥有无限 cloud budget 的 teams,这可能很好用。但有些时候,需要 incrementally process data,使 pipelines 能够在 reasonable amount of time 内交付 data。Reasonable amount of time 是任意的,由你和 team 决定;但如果你无法在这个时间框架内处理 data,无论它是什么,就可能需要考虑 incremental materialization。
我们说 incrementally processed 到底是什么意思?当我们谈论 incremental processing data 时,指的是只转换尚未被转换过的数据,然后将这些 newly transformed data 添加到 target table。例如,假设你有一张 source table,存储 ecommerce website 相关 orders data,我们需要转换这些 data,并存入一张 table 供 analytics team 使用。但这张 table 有数十亿 records,因此每次 invoke dbt 都重新创建 target table 并不现实,因为 build 会耗费数小时。相反,我们可以先 build table 一次,然后在之后的 dbt invocations 中,只转换 prior run 之后新增的数据,再将这些 data upsert 到 target table。本节剩余部分会讨论在 dbt 中实现 incremental transformations 的不同方式,以及什么时候使用不同 patterns 和 strategies。
与所有其他 materialization types 一样,incremental models 也是通过编写 select statement,并设置 configurations 来告诉 dbt 该 model 应 incrementally materialized。不过,使用 incremental materialization 比 view 或 table materialization 更复杂,因此只有当 optimization benefits 超过 development 和 maintenance 成本时,才应转向 incremental approach。通常,当普通 table materialization 无法满足 pipeline SLAs 时,我们才会转向 incremental materialization。
理解 incremental models 的 syntax 需要一些适应,尤其是如果你来自过去使用 stored procedures 增量加载 tables 的世界。在 dbt 之前,如果你想在 data transformation 后 incrementally load table,可能会采用类似模式:
- 确定 destination table 的 state,也就是上一次 data 被 load 到其中是什么时候。
- Transform new data,并将其 insert 到 temporary table。
- 将 temporary table 中的数据 merge 或 upsert 到 destination table。
幸运的是,使用 incremental models 可以简化这个过程,我们只需要关注编写 select statement 来 transform data。不需要将其存储到 temporary table,也不需要编写 merge / upsert logic。与大多数事情一样,dbt 会为你处理所有 boilerplate code。
Caution
下面的示例配置为针对 Snowflake 运行,并且默认情况下,dbt-snowflake adapter 使用 merge strategy 构建 incremental models。因此,在接下来的几个示例中,我们会从这个角度讨论 incremental models。如果你使用 Snowflake 以外的 database,请查看对应 database 的 dbt adapter documentation,了解它使用哪种 incremental strategy。这会为你理解 incremental models 提供基础,但本章后面还会讨论 dbt 提供的每种 incremental strategies,以及如何配置它们。
先从一个简单示例开始,看看如何在 dbt 中使用 incremental materialization 实现 model。这个示例会生成一个 model,用于存储 order revenue 相关数据。如果你想跟着操作,该示例位于本书 repo 的路径:
~/models/marts/finance/fct_revenue_orders.sql
也可在 Listing 4-7 中看到。
{{
config(
materialized = 'incremental',
unique_key = 'OrderId'
)
}}
with get_orders_revenue as(
select
pro.OrderId,
sum(pro.Price) as Revenue
from {{ ref('int_orders_items_products_joined') }} as pro
group by 1
)
select
ord.OrderId,
ord.OrderPlacedTimestamp,
ord.UpdatedAt,
ord.OrderStatus,
ord.SalesPerson,
rev.Revenue
from get_orders_revenue as rev
join {{ ref('stg_furniture_mart_orders') }} as ord
on rev.OrderId = ord.OrderId
Listing 4-7:基础 incremental dbt model 示例
这个示例与本章覆盖的其他 model 示例类似,开头是一个 config block。在 config block 中,materialization configuration 设置为 'incremental',但注意还有一个 additional configuration:unique_key。dbt 使用这个 configuration 来确定哪一列构成 table 的 grain,并使用该列执行 merge 或 upsert operation,以便将 new records insert 到 destination table,并 update existing records。在 Listing 4-7 中,unique_key 设置为 OrderId,因此 dbt 会期望我们的 select query 每个 OrderId 只返回一行。
我们想指出,dbt 并不要求 incremental models 必须设置 unique_key configuration。如果没有设置 unique_key,dbt 会 insert 你的 select statement 返回的每一条 record,而不是运行 merge / upsert operation。大多数时候,这并不是理想行为,因为 insert statements 本质上不是 idempotent。因此我们建议除非有充分理由不包含,否则始终包含 unique_key configuration。
Definition
在 data pipelines 语境中,Idempotence 指的是:无论 pipeline 在何时或运行多少次,相同 inputs 都应始终产生相同 outputs。因此,insert operations 如果没有对应 delete,通常不具备 idempotence。
除了 config block,到目前为止,这个 model 的设置与本章覆盖过的其他 model 示例一样。但我们来看一下使用 incremental materialization 运行 dbt 时实际发生什么。
在 initial run 中,dbt 会运行一个 statement 来创建 table,如 Listing 4-8 所示。这当然是必要的,因为如果不存在 existing table,dbt 就没有任何东西可以 incrementally build into。
create or replace transient table dbt_learning.public.fct_revenue_orders as
(
with get_orders_revenue as(
select
pro.OrderId,
sum(pro.Price) as Revenue
from public.int_orders_items_products_joined as pro
group by 1
)
select
ord.OrderId,
ord.OrderPlacedTimestamp,
ord.UpdatedAt,
ord.OrderStatus,
ord.SalesPerson,
rev.Revenue
from get_orders_revenue as rev
join public.stg_furniture_mart_orders as ord
on rev.OrderId = ord.OrderId
);
Listing 4-8:incremental model 首次运行时 dbt 在 database 中执行的 query
然而,在 subsequent runs 中,dbt 会触发不同流程,使 records incrementally inserted,而不是对 target table 进行 full rebuild。不过,在剖析 incremental build 期间 database 中运行哪些 commands 前,你可能会好奇:dbt 如何知道这事实上是 incremental build?
每次 invoke dbt 时,不只是 incremental models,都会向 database 发送一组 commands,以了解已经存在什么 objects。这些 commands 会因 target database 配置不同而不同;但对于 Snowflake,也就是我们示例中使用的平台,dbt 会运行以下 commands 的组合:
show terse objects in database <target.database>
show terse objects in <target.database>.<target.schema>
describe table <target.database>.<target.schema>.<some_table_name>
虽然这些 commands 是 Snowflake adapter 特定的,但对于所有 adapters,dbt 实际上都是从 database 中抓取 metadata,以理解哪些 objects 已经存在。通过这样做,dbt 能够在 run 期间 cache metadata,从而判断需要自动替你执行哪些 actions,例如:
-
Generating schemas
-
首次构建 incremental models 或 incrementally 构建
-
首次构建 snapshots 或 incrementally 构建
-
知道 dbt 会在每次 invocation 开始时抓取这些 metadata 后,在 subsequent run 中,dbt 会知道
fct_revenue_orderstable 已经存在,因此能够推断这必然是该 model 的 incremental build。所以,dbt 不会对这张 table 运行create or replacestatement,而是会执行以下操作:- 使用 model file 中定义的 SQL 创建 temporary view
- 运行 merge statement,将 view 的结果 merge 到 target table 中
- Drop temporary view
Listing 4-9 展示了 dbt 会为这三个步骤执行的 commands。
##Step 1
create or replace temporary view dbt_learning.public_finance.fct_revenue_orders__dbt_tmp as (
with get_orders_revenue as(
select
pro.OrderId,
sum(pro.Price) as Revenue
from public_staging.int_orders_items_products_joined as pro
group by 1
)
select
ord.OrderId,
ord.OrderPlacedTimestamp,
ord.UpdatedAt,
ord.OrderStatus,
ord.SalesPerson,
rev.Revenue
from get_orders_revenue as rev
join public_staging.stg_furniture_mart_orders as ord
on rev.OrderId = ord.OrderId
);
##Step 2
merge into dbt_learning.public_finance.fct_revenue_orders as DBT_INTERNAL_DEST
using dbt_learning.public_finance.fct_revenue_orders__dbt_tmp as DBT_INTERNAL_SOURCE
on (DBT_INTERNAL_SOURCE.OrderId = DBT_INTERNAL_DEST.OrderId)
when matched then update set
"ORDERID" = DBT_INTERNAL_SOURCE."ORDERID",
"ORDERPLACEDTIMESTAMP" = DBT_INTERNAL_SOURCE."ORDERPLACEDTIMESTAMP",
"UPDATEDAT" = DBT_INTERNAL_SOURCE."UPDATEDAT",
"ORDERSTATUS" = DBT_INTERNAL_SOURCE."ORDERSTATUS",
"SALESPERSON" = DBT_INTERNAL_SOURCE."SALESPERSON",
"REVENUE" = DBT_INTERNAL_SOURCE."REVENUE"
when not matched then insert
("ORDERID", "ORDERPLACEDTIMESTAMP", "UPDATEDAT", "ORDERSTATUS", "SALESPERSON", "REVENUE")
values
("ORDERID", "ORDERPLACEDTIMESTAMP", "UPDATEDAT", "ORDERSTATUS", "SALESPERSON", "REVENUE")
;
##Step 3
drop view if exists dbt_learning.public_finance.fct_revenue_orders__dbt_tmp cascade
Listing 4-9:incremental model subsequent runs 中 dbt 执行的 commands 示例
Listing 4-9 中展示的步骤是 Snowflake adapter 特定的,因此其他 adapters 可能略有不同。不过,整体概念保持一致:dbt 会替你处理 merge / upsert logic。无论如何,理解 dbt 如何 incrementally build models 的需求已经被抽象掉了,因此作为 dbt user,你其实不需要太担心这些。我们之所以覆盖这些信息,是因为我们认为如果你在读这本书,可能会对幕后发生的事情感兴趣,想比标准 dbt documentation 更深入一点。
在继续之前,我们想提醒你注意:在这个示例中,我们没有做任何事情来限制每次 dbt run 中被 transformed 的 data。因此,每次该 model 执行时,model 查询的所有 underlying data 都会被 transform,然后 merge operation 会将这些 data upsert 到 target table。更高性能的做法是,在 select new data to upsert into target table 之前,先理解 target table 的 state。与大多数事情一样,dbt 提供了解决 incremental builds 中限制 data retrieval 问题所需的 features。
Limiting Data Retrieval
运行 incrementally build tables 的 data transformations 时,出于 performance 原因,你通常希望限制每次 pipeline run 中被 transform 的数据量。重新处理你知道已经存在于 target table 中、且自 pipeline 上一次运行以来未发生变化的数据,是低效的。如果不限制 data retrieval,就会无意中增加 database 扫描 source 和 target tables 的次数。事实上,在不限制 data retrieval 的情况下 incrementally build tables,通常会比简单重建 target table 更差,因为 scan 次数增加了。到目前为止,本节中我们看到的 dbt incremental materialization 示例都没有考虑这一点。现在来看如何解决这个问题,并改进 incremental models 的 optimization。
幸运的是,dbt 提供了几种 built-in methods,让你能够限制在 subsequent invocations 中使用 incremental materialization 的 models 将要 transformed、processed 或 scanned 的 data。第一个 feature 是 is_incremental macro,它允许你在 model files 中编写 additional code,而这些 code 只会在 model subsequent runs 中被包含。Incremental models 中的常见 pattern,是在 is_incremental macro 的上下文中添加 where clause,根据 target table 中的 maximum timestamp 限制 model select statement 返回的数据。Listing 4-10 展示了如何使用这种 pattern 扩展本节一直使用的 incremental model fct_revenue_orders 的 functionality。
{{
config(
materialized = 'incremental',
unique_key = 'OrderId'
)
}}
with get_orders_revenue as(
select
pro.OrderId,
sum(pro.Price) as Revenue
from {{ ref('int_orders_items_products_joined') }} as pro
group by 1
)
select
ord.OrderId,
ord.OrderPlacedTimestamp,
ord.UpdatedAt,
ord.OrderStatus,
ord.SalesPerson,
rev.Revenue
from get_orders_revenue as rev
join {{ ref('stg_furniture_mart_orders') }} as ord
on rev.OrderId = ord.OrderId
{% if is_incremental() %}
where ord.UpdatedAt > (select max(UpdatedAt) from {{ this }})
{% endif %}
Listing 4-10:使用 is_incremental macro 的示例
Listing 4-10 的示例是本章前面 Listing 4-7 的扩展。除了最后三行代码,其余内容都一样。如前所示,我们通过 Jinja if statement 评估 is_incremental macro 返回的值。我们可以使用 if statement 告诉 dbt 什么时候在 compiled SQL 中包含 where statement,因为 is_incremental macro 返回 boolean value,并且在以下条件满足时 evaluates to true:
- Target table 存在于 database 中。
- Model materialization 配置为 incremental。
- dbt 没有以 full refresh mode 运行该 model。
Note
使用 incremental models 时,可能会遇到 data 失去同步的场景。这可能由很多原因造成,例如 logic 变化或引入 bug。幸运的是,dbt 有 built-in flag,可以传给 dbt commands,从而从头重建 incremental models。如果需要这样做,只需像下面这样 select model:
dbt run --select my_incremental_model --full-refresh
使用 is_incremental macro 动态 include 或 exclude where clause,以基于 timestamp 过滤 data,是最常见的 pattern,但这并不是该 macro 的唯一用法。事实上,在它的上下文中可以放入任何合法 SQL。因此,如果某部分 transformation 只希望在 incremental models 的 subsequent runs 中运行,可以大胆实验。例如,当构建包含 window 或 aggregate functions 的 incremental models 时,这可能特别有帮助。
Definition
在这个示例中,我们使用 {{ this }} function,它会在 compile time 被替换为 target table 的 fully qualified name。我们会在第 6 章更深入讨论这个 function。现在只需要知道,它是一种引用 target table 的方便方式,不需要在 model definition 中 hardcode table name。
使用 is_incremental macro 限制 incremental model 中 select statement 返回的数据,可以提升 performance,因为 database 可能扫描更少 upstream data。然而,这只是 dbt 提供的帮助 incremental models 限制 data retrieval 的一个 feature。还有第二个 feature 可以用来提升 incremental models performance,它通过限制 target table 中被 scanned 的 data 实现。这通过 incremental_predicates configuration 实现,该 configuration 接收一个 string value,表示一段合法 SQL string。dbt 会将它注入为你生成的 SQL 中,用来 insert 和 update incremental model 的 target table 中的数据。
Caution
incremental_predicates configuration 只在使用 merge strategy 时有效。
Listing 4-11 中,我们通过使用 incremental_predicates configuration,进一步迭代本节一直使用的示例。先看看如何使用这个 configuration,然后再讨论。
{{
config(
materialized = 'incremental',
unique_key = 'OrderId',
incremental_predicates=['DBT_INTERNAL_DEST.UpdatedAt > dateadd(day, -7, current_date())']
)
}}
with get_orders_revenue as(
select
pro.OrderId,
sum(pro.Price) as Revenue
from {{ ref('int_orders_items_products_joined') }} as pro
group by 1
)
...
Listing 4-11:使用 incremental_predicates configuration 的示例
在 Listing 4-11 中,我们使用这段 SQL 设置 configuration:
'DBT_INTERNAL_DEST.UpdatedAt > dateadd(day, -7, current_date())'
当 dbt 运行 merge statement,将 data incrementally load 到 destination table 时,会将这个 predicate 作为必须满足的 condition 注入其中。这个 statement 的作用是限制 target table 中的 data,只考虑过去七天内被 updated 的 data 参与 merge。这可以提升 performance,因为 database optimizer 不需要扫描 target table 中的每条 record 或 partition 来判断某条 record 是否符合 merge 条件。Listing 4-12 展示了添加该 predicate 后,merge statement 会是什么样子。
merge into dbt_learning.public_finance.fct_revenue_orders as DBT_INTERNAL_DEST
using dbt_learning.public_finance.fct_revenue_orders__dbt_tmp as DBT_INTERNAL_SOURCE
on (DBT_INTERNAL_DEST.UpdatedAt > dateadd(day, -7, current_date()))
and (
DBT_INTERNAL_SOURCE.OrderId = DBT_INTERNAL_DEST.OrderId
)
when matched then update set
...
when not matched then insert
...
;
Listing 4-12:注入 defined incremental_predicate 后由 dbt 生成的 merge statement
注意,在我们定义的 predicate 中,我们在 UpdatedAt column 前使用了 alias DBT_INTERNAL_DEST。这样做是为了让 database 知道我们希望 predicate 应用于哪一列,因为这是 merge statement,如果不使用 alias 前缀,UpdatedAt 会是 ambiguous column name。因此,在 predicates 中应使用 DBT_INTERNAL_DEST 和 DBT_INTERNAL_SOURCE 为 columns 加前缀,具体取决于你希望 predicate 如何工作。
在这个示例中,我们使用 DBT_INTERNAL_DEST prefix,因为我们只希望在 destination table 中的 UpdatedAt column 位于最近七天内时,才考虑这些 data 参与 merge。虽然示例中的 data 很小,但在类似操作中,这可能带来有价值的 performance gains,并减少 destination table scans。话虽如此,请记住,dbt 不会验证你在 incremental_predicates configuration 中定义的 SQL 的 logic 或 validity,因此这应被视为 dbt 的 advanced usage。因此,我们建议只有在处理大量 data,并且收益大于 implementation complexity 时才使用该 feature。正如我们之前提到的,尽可能久地保持简单!
Incremental Strategies
到目前为止,本节从 merge strategy 角度讨论了 incremental models 如何工作。我们这样做,是因为它是 Snowflake 的默认策略,而 Snowflake 是与 dbt 搭配使用的非常流行的 data platform,也是我们构建示例的平台。不过,我们完全理解不是每个人都会使用 Snowflake,而且 merge strategy 也不适用于所有 incrementally loading data 的 use case。dbt 也认识到这一点,因此为了应对这种情况,dbt ecosystem 中有四种 strategies 可以配置 incremental models 的行为。表 4-2 展示了这些 strategies、它们如何工作,以及 implementation complexity。
表 4-2:dbt incremental strategies
| Incremental Strategy | Complexity | Use Case |
|---|---|---|
| Merge | Low | Data 需要被 inserted 和 updated 到 destination tables,但不需要 deleted。Uniqueness 对你很重要 |
| Append | Low | Data 只需要 inserted 到 destination tables。Uniqueness 对你不重要 |
| Delete+Insert | Medium | Data 需要被 inserted 和 updated 到 destination tables,但不需要 deleted。Uniqueness 对你很重要 |
| Insert Overwrite | High | Data 需要被 inserted 和 updated 到 destination tables。Uniqueness 对你很重要。Data 必须存储在 partitions 中,因为每次运行都会替换 / truncate partitions |
由于本节前面已经通过示例讲解了 merge incremental strategy,现在继续看其他 strategies 如何工作。我们首先展示如何配置 delete+insert 和 append strategies,但本节剩余大部分内容会通过一个示例讲解如何使用 insert overwrite strategy。
要使用 delete+insert,需要对 config block 做的修改非常直接,因为只需要设置 incremental_strategy configuration。除此之外,config block 和 model 其余部分保持不变。Listing 4-13 展示了配置 delete+insert strategy 的 syntax。
{{
config(
materialized = 'incremental',
unique_key = 'OrderId',
incremental_strategy = 'delete+insert'
)
}}
...
Listing 4-13:配置 incremental model 使用 delete+insert strategy 的 syntax
注意,我们仍然为 delete+insert strategy 定义了 unique_key configuration,因为 dbt 需要理解该 table 的 grain,才能适当地 delete 或 insert records 到 destination table。这个 strategy 的工作方式是:当 records 被包含在 transformation 中时,会先从 target table 中删除这些 records,然后重新 insert 它们。在我们看来,用 visual 更容易理解。图 4-1 展示了该 strategy 的工作方式。图中 source table 有三条 records,但 target table 当前只有两条。在该 model 的 incremental run 中,records 2 和 3 被 transformed。因此,record 2 会从 target table 中删除,然后与 record 3 一起重新 insert。最终结果是 target table 中包含全部三条 records。
图 4-1:delete+insert incremental strategy 中 data flow
下一个 strategy 是 append strategy。如果查看 Listing 4-14,会注意到使用 append strategy 时没有定义 unique_key。使用 append strategy 时,dbt 不需要知道 destination table 的 grain,因为 data 会通过 insert statement 被 incrementally loaded。因此,dbt 和 database 都不会检查该 table 中是否存在 duplicate records。如果 duplicate data 有任何可能进入 target table,我们建议避免使用这种 strategy。此外,existing data 不会被 deleted 或 updated,所以在决定哪种 incremental strategy 适合 model 时,必须记住这一点。
{{
config(
materialized = 'incremental',
incremental_strategy = 'append'
)
}}
...
Listing 4-14:配置 incremental model 使用 append strategy 的 syntax
最后要覆盖的 incremental strategy 是 insert_overwrite strategy,它可用于 BigQuery 和 Spark 等 adapters。这个 strategy implementation 最复杂,但如果设置正确,可能带来最大的 performance benefit。这个 strategy 最类似 merge strategy,但区别在于,它不是在 destination table 中 insert 和 update records,而是替换并创建 destination table 的 entire partitions。由于该 strategy 以这种方式使用 partitions,因此 incremental loading table 时所需 scans 数量会显著减少。这是因为只有你指示 dbt 交互的 partitions 会被扫描,因此处理非常大数据量时可以看到 performance improvements。如果还不明显,这个 strategy 只有在你已经将 target table 设置为基于某个 column values partitioned 时才有效。常见做法是使用 timestamp 或 date column 来 partition tables。
Tip
如果你处理的数据量不大,但仍然想使用 incremental materialization,我们建议从 merge strategy 开始,只有当 implementation complexity 相对于 performance gains 是合理的,才转向 insert_overwrite。
使用 insert_overwrite strategy 时,有两种方式配置哪些 partitions 会被 overwritten:
Static partitions
你告诉 dbt 要替换哪些 partitions。
Dynamic partitions
你让 dbt 判断要替换哪些 partitions。
第一个使用 insert_overwrite strategy 的示例中,我们会配置 model 使用 dynamic partitions method。在我们看来,这是学习这种 incremental strategy 的最佳起点,因为它是两种方法中更容易 implementation 的一种。该示例位于本书 repository 的路径:
~/models/marts/finance/fct_revenue_orders_insert_overwrite_dynamic.sql
值得注意的是,我们使用 enabled configuration 禁用了该 model,因为它只适用于特定 adapters。不过,本章后面会讨论这一点以及许多其他 model configurations。
Note
虽然本书大多数示例使用 Snowflake syntax,但下面两个示例使用 BigQuery syntax,因为 Snowflake 不支持 insert_overwrite strategy。
Listing 4-15 中的代码应该看起来很熟悉,因为它也是对本节一直使用的 incremental model 的另一个变体。不过,该 model 的这个 variation 在代码写法上有几个区别。
{{
config(
materialized = 'incremental',
partition_by = {
'field': 'UpdatedAt',
'data_type': 'timestamp'
},
incremental_strategy = 'insert_overwrite',
enabled = false
)
}}
with get_orders_revenue as(
select
pro.OrderId,
sum(pro.Price) as Revenue
from {{ ref('int_orders_items_products_joined') }} as pro
group by 1
)
select
ord.OrderId,
ord.OrderPlacedTimestamp,
ord.UpdatedAt,
ord.OrderStatus,
ord.SalesPerson,
rev.Revenue
from get_orders_revenue as rev
join {{ ref('stg_furniture_mart_orders') }} as ord
on rev.OrderId = ord.OrderId
{% if is_incremental() %}
where date(ord.UpdatedAt) >= date_sub(date(_dbt_max_partition), interval 2 day)
{% endif %}
Listing 4-15:使用 insert_overwrite strategy 和 dynamic partitions method 的 incremental model 示例
先回顾 config block:
{{
config(
materialized = 'incremental',
partition_by = {
'field': 'UpdatedAt',
'data_type': 'timestamp'
},
incremental_strategy = 'insert_overwrite'
)
}}
Listing 4-15 的 config block 与本节看到的其他示例非常相似。当然,incremental_strategy 被设置为 insert_overwrite,但也请注意加入了 partition_by configuration。如前所述,这个 strategy 通过替换 entire partitions 工作,因此 dbt 会根据该 configuration 中的 partition definition 构建 tables。partition_by configuration 接收一个 dictionary,其中定义了按哪个 field partition,以及该 field 的 data type。换句话说,当 dbt 构建这张 table 时,会按 UpdatedAt timestamp partition。Partitioning tables 可能是一个复杂主题,因此我们建议参考你的 data platform documentation,确定 tables 应如何 partition。总体而言,通过包含 partition_by dictionary 并将 strategy 设置为 insert_overwrite,model 就已经准备好利用该 strategy。
{% if is_incremental() %}
where date(ord.UpdatedAt) >= date_sub(date(_dbt_max_partition), interval 2 day)
{% endif %}
正如前面讨论的,这个示例使用 dynamic partitions strategy,这意味着让 dbt 判断哪些 partitions 应被替换。这是通过前面看到的 _dbt_max_partition variable 完成的。当 model 运行时,该 variable 会被替换为 destination table 中 maximum partition 对应的 timestamp。此外,该 timestamp 会使用 date_sub function 稍作转换,使我们能够有效 insert 或 overwrite table 中 maximum partition 日期,以及再往前两天的 partitions。
但不要认为你被绑定在这个 logic 上。实际上,incremental logic 完全可配置。例如,你可能更频繁地运行这个 transformation,并且只需要 create 或 replace 过去三小时的 partitions。你需要理解自己的 data needs,决定应该创建或替换哪些 partitions。
Tip
is_incremental macro 对 insert_overwrite strategy 的工作方式,与对其他所有 incremental strategies 的工作方式相同。
前一个示例中,我们选择使用 dynamic method 运行 insert_overwrite strategy,也就是让 dbt 根据 destination table 中的 maximum partition 判断要替换哪些 partitions。Dynamic method implementation 简单,但处理大数据量时会带来一些 performance implications,因为其中包含一个额外步骤:dbt 需要 query destination table 来确定 maximum partition。不过,这个限制可以通过使用 static method 克服。Listing 4-16 展示了使用该方法的示例,也可以在 repo 中找到:
~/models/marts/finance/fct_revenue_orders_insert_overwrite_static.sql
{% set partitions_to_replace = [
'timestamp(UpdatedAt)',
'timestamp(date_sub(UpdatedAt, interval 2 day))'
]
%}
{{
config(
materialized = 'incremental',
partition_by = {
'field': 'UpdatedAt',
'data_type': 'timestamp'
},
incremental_strategy = 'insert_overwrite',
partitions = partitions_to_replace
)
}}
with get_orders_revenue as(
...
{% if is_incremental() %}
where timestamp_trunc(ord.UpdatedAt, day) in ({{ partitions_to_replace | join(',') }})
{% endif %}
Listing 4-16:使用 insert_overwrite strategy 和 static partitions method 的 incremental model 示例
这个示例中的第一个区别,如下面代码所示,是在 model 顶部定义了一个 Jinja variable。该 variable 用于存储我们希望在该 model 运行时替换的 partitions list。在这个示例中,我们定义希望替换过去三天的 partitions,也就是今天、昨天和前天。这不同于 dynamic method,因为在判断要替换哪些 partitions 时,并不会考虑 destination table 中的 maximum partition。
{% set partitions_to_replace = [
'timestamp(UpdatedAt)',
'timestamp(date_sub(UpdatedAt, interval 2 day))'
]
%}
两种确定要替换 partitions 的方法之间另一个显著差异,是 model incremental runs 时包含的 block。注意下面代码中,我们不是使用 _dbt_max_partition variable,而是创建一个 where clause,检查 record 的 date 是否位于我们定义的 list,也就是 partitions_to_replace 中:
{% if is_incremental() %}
where timestamp_trunc(ord.UpdatedAt, day) in ({{ partitions_to_replace | join(',') }})
{% endif %}
为了生成正确 SQL syntax,我们使用 join() function,使 dates 成为 comma-delimited list。例如,如果今天是 2023-01-03,那么 where clause 会 compile 为:
where timestamp_trunc(ord.UpdatedAt, day) in ('2023-01-03', '2023-01-02', '2023-01-01')
最后一点,我们建议只有当 dynamic partitions method 的 performance 不够时,才转向 static partitions method。更一般地说,incremental materialization 是 dbt 中一个强大工具,但我们建议谨慎使用。Incrementally building tables 会为 data pipelines 增加额外 complexity,因此如果你是 small team 或处理的数据量较小,那么额外 complexity 可能不值得。如果是这种情况,我们建议继续使用 table 和 view materializations 来完成 data transformations。
Ephemeral
dbt 当前可用的最后一种 SQL model materialization 是 ephemeral materialization。Ephemeral models 的工作方式是:允许你编写 SQL,将其存储在 .sql file 中,然后 dbt 会使用 CTE(common table expression)将这段 SQL 注入任何引用它的 models 中。换句话说,dbt 并不会在 database 中真正 materialize ephemeral models,而是将它们作为一种方式,让你 modularize repeatable code,并在其他 downstream models 中引用。
Reminder
使用 ephemeral materialization 的 models 不会作为 objects 持久化在 database 中!
让我们快速看一个示例,了解它如何工作,然后讨论什么时候我们认为使用 ephemeral materialization 而不是其他 materialization 是合理的。Listing 4-17 展示了如何定义 ephemeral model,并在 downstream model 中使用它。
{{
config(
materialized = 'ephemeral'
)
}}
select
*
from {{ source('source_a', 'table_a') }}
/*************/
#Above, is an ephemeral model selecting from a source.
#Below, is a table model that includes the ephemeral model as a reference.
/*************/
{{
config(
materialized = 'table'
)
}}
select
*
from {{ ref('my_view_model') }} as my_view_model
join {{ ref('my_ephemeral_model') }} as my_ephemeral_model
on my_view_model.id = my_ephemeral_model.id
Listing 4-17:定义 ephemeral model 并在 downstream table model 中使用的示例
如 Listing 4-17 所示,ephemeral models 与所有其他 SQL models 一样,定义在 .sql files 中,并且可以使用 ref function 在 downstream 中引用。Model file 看起来与任何其他 model file 相同。但如果查看 Listing 4-18,就会看到 dbt 如何将 ephemeral model 中的 SQL 注入 downstream model 的 compiled SQL 中。
with __dbt__cte__my_ephemeral_model as(
select
*
from source_database.source_a.table_a
)
select
*
from dbt_learning.public.my_view_model as my_view_model
join __dbt__cte__my_ephemeral_model as my_ephemeral_model
on my_view_model.id = my_ephemeral_model.id
Listing 4-18:Listing 4-17 第二部分中定义的 table model 的 compiled SQL
在示例第二部分中,原本引用 ephemeral model 的 join 被替换为引用 CTE 的 join。这是一个值得了解的不错特性,但根据我们的经验,通常有比 ephemeral materialization 更好的 materializations 可用。
在我们看来,ephemeral models 唯一有用的场景,是当一个 model 开始在同一文件中存储大量代码时。此时,你可以考虑将一部分 logic 上提一层,放入一个或两个 ephemeral models 中。但要注意,这只是为了 readability。Ephemeral models 不太可能为你带来任何 performance improvement。此外,如果你在 downstream 中被多个 models 引用 ephemeral models,我们建议考虑将该 model 转换为 view 或 table materialization。
Python Models
从一开始,dbt 就被构建为一个 framework,用于将 software development best practices 引入 analytics 世界。很长一段时间,这意味着你可以使用 dbt 通过 SQL 构建 models。从战略上看,这很合理,因为 SQL 长期以来都是 data professionals 的 lingua franca。然而,dbt 早期已经过去,这个工具被证明适用于广泛 use cases。虽然 SQL 通常是 data transformation 的最佳工具,但并非总是如此。因此,应大众需求,dbt 开始支持 Python models,覆盖 cloud data platform landscape 中的三大玩家:Snowflake、BigQuery 和 Databricks。
通过使用 Python models,你可以使用许多 data engineers 和其他 data professionals 熟悉的 syntax 编写 data transformations。对于 BigQuery 和 Databricks,Python models 使用 PySpark framework 编写;但 Snowflake 略有不同,使用它自己的 framework,称为 Snowpark。虽然两个 frameworks 不同,但它们有许多相似之处,因此可以从根本上以同一种方式讨论 Python models。无论你使用哪个 data platform,dbt 中的 Python models 都表示为一个 function,该 function 对 dataframe object 进行操作,并返回一个 dataframe object。如果你以前用过 Pandas、PySpark 或 Polars,那么已经熟悉 dataframe 概念。如果你是 dataframes 新手,那么最简单地说,可以把它们看作 table-like object,或者 rows 和 columns data 的表示。
同样与 SQL models 类似,dbt 不会在自己的 infrastructure 上执行 Python models,而是会正确 compile code,并将其发送到 data platform 执行。话虽如此,由于 Python models 会在 data platform 的 infrastructure 上执行,你需要确保已正确设置 data platform,以支持接受 Python jobs。这可能从简单启用 Python capabilities,到定义 Python-specific compute clusters 不等。截至本书写作时,Python models 仍然是非常新的 feature,因此我们建议直接参考 dbt documentation,获取确保能够运行 Python models 的进一步说明: docs.getdbt.com/docs/build/…
Writing a Python Model
SQL models 和 Python models 的编写方式存在很多巨大差异,但两种 model types 之间也有一些相似概念。首先,Python models 与 SQL models 一样存储在 models directory 中。这很有用,因为无论 models 是 SQL 还是 Python,你仍然可以将相似 models 存放在相同 directories 中。如果 Python models 必须以根本不同的方式存储,那会非常麻烦。
其他相似点包括:引用 upstream models 和 sources、设置 model-level configurations,以及之后可以在其他 downstream models 中引用 Python models,无论 downstream 是 Python 还是 SQL 都无所谓。看 Listing 4-19,了解如何定义一个非常简单的 Python model。
def model(dbt, session):
dbt.config(materialized='table')
raw_orders_df = dbt.ref('raw_products')
final_df = raw_orders_df.WithColumn(
'close_out_price',
raw_orders_df.price * 0.6
)
return final_df
Listing 4-19:基础 Python model syntax 示例
分解这个示例,从第 1 行开始:
def model(dbt, session):
每当编写 Python model 时,返回 dataframe 并在 database 中 materialized as table 的 function 必须始终命名为 model。随后,这个 function 始终接收两个 positional arguments。这些 arguments 的用途如下:
dbt
来自 dbt Core 的 Python class,使该 function 能作为 dbt project 中的 model 暴露。
Session
用于连接 data platform 的 Python class。这是 model function 内读取 data 到 dataframes,以及将返回的 dataframes 写入 database tables 的方式。
dbt.config(materialized='table')
接下来,注意在 function 内部,我们直接访问 dbt class 的 config method。这里,我们定义 model 返回的 dataframe 应在 database 中 materialized as table。这个 config method 的工作方式类似 SQL models 中的 config block,但目前限制更多。关于 Python models 中可设置 configs 的最新完整列表,我们建议查看 dbt documentation。
此外,请注意,我们将 model 配置为使用 table materialization。如果你还记得本章前面,SQL models 有多种 materializations 可用。Python models 也是如此,但 materializations 范围更有限。事实上,你只能使用 table 或 incremental materialization 配置 Python models。不过,仔细想想这是合理的,因为 Python models 返回的是实际 dataset,因此配置为 view 或 ephemeral materialization 并不合理。此外,这两种 materialization types 本质上基于 SQL,在 Python model context 中没有意义。
raw_orders_df = dbt.ref('raw_products')
现在 function 已创建并配置完成,我们需要要转换的数据。可以通过 dbt class 的 ref method 从 upstream models 获取 data。这里,我们从 upstream model raw_products 获取 data,并将其存储到 dataframe 中。由于引用的是 upstream model,所以使用 ref method;但 dbt class 也有 source method,如果需要直接从 source table 获取 data,可以使用它。
这个示例剩余部分中,我们通过添加一个新 column 执行简单 transformation,该 column 表示 product 的 closeout price,其中 closeout price 是 40% discount。然后,我们将完整 resulting dataframe 存储在名为 final_df 的新 dataframe 中。这个 dataframe 是 dbt invoked 时 function 返回的对象,其结果会作为 table 存储在 database 中。正如这个示例中所做,为了让 Python models 正确 materialize,它们应该始终返回单个 dataframe object。
我们故意让这个示例很简单,因为这里只想覆盖定义 Python models 的关键部分,例如 function naming convention、expected positional arguments 和 expected return values。根据你使用的 data platform 不同,实际编写 Python transformations 的 syntax 差异太多。不过,我们确实鼓励你探索使用 Python models,因为它们可以成为标准 SQL models 的强大补充。
When to Use Python Models
我们希望能很容易划清界线,说 “X 用 Python,Y 用 SQL”,但遗憾的是,现在并没有这么简单。这主要是因为截至写作时,Python models 仍然是 dbt 的一个极新 feature。因此,我们在自己的 projects 中使用大量 Python models 的 firsthand experience 并不多。这并不是说它们没有位置,因为它们当然有。
从我们的立场来看,在 SQL model 和 Python model 之间做选择时,我们建议默认使用 SQL models,只有当代码用 Python 执行更高效,或 development process 更简单时,才转向 Python model。例如,如果你正在编写一个包含大量 Jinja 的 SQL transformation,比如 dynamic pivot,而你可以使用 PySpark 或 Snowpark syntax 来简化 transformation,那么将该 model 写成 Python model 可能是合理的。
最后,我们认为 Python models 最有价值的 use case,是更广泛地为 data science 和 machine learning teams 打开大门。在 Python models 加入之前,如果这些 teams 想使用 dbt SQL model 输出的数据训练 machine learning model,就需要使用某个 external tool 管理运行 Python code 的 infrastructure,甚至可能还需要另一个工具来 orchestrate 这个过程。但随着 Python models 加入,这些 teams 可以轻松集成到其他 data teams 使用的同一 stack 中,并在合理场景下使用 dbt 构建和编排 machine learning models。
总体而言,将 Python 加入 dbt 是扩展 dbt capabilities、让该工具触达更广泛 audience 的重大进步。不过,我们鼓励你只有在编写 SQL model 太复杂,或者 SQL 无法提供 transformation 所需能力时,才使用 Python models。
Modular Data Transformations
到目前为止,本章讨论了如何构建 models;前一章讨论了如何定义 sources 和 seed files。我们也覆盖了 models 如何引用其他 upstream sources、seeds 和 models。如果你还记得第 2 章,我们讨论过 project structure 主题。在那一节中,我们给出了如何组织 project 中 models 的建议。请记住,结构化 dbt project 不止一种方式,没有哪一种绝对正确或错误。话虽如此,许多 teams 在构建 models 时喜欢遵循 staging-intermediate-marts pattern。在这种 pattern 中,你最终会拥有三类 models:
Staging models
这些 models 与 sources 一对一。它们用于执行非常轻量的 transformations,例如 cast data types 和 renaming columns。Staging models 不应包含任何 joins,并且除了 staging models 之外,任何其他 model type 都不应直接从 sources select。
Intermediate models
这些 models 应从 upstream staging models select,并执行 downstream transformation pipeline 中多个 models 会复用的 transformations。
Mart models
这些包括 fact 和 dimension models,应表示在 data warehouse 中暴露给 end users 的 models。这些 models 应是 staging 和 / 或 intermediate models transformation 的结果。
将 dbt models 构建成这三类,非常契合 modularity 概念。Modularity 是 Software Engineering 中的一个概念和 best practice,它认为 code 应该拆成 reusable chunks,使一组 logic 只实现一次,然后复用。通过使用 modularity,以 logical steps 编写 data transformations,可以获得几个好处:
- Transformations 可以在 downstream models 中复用。不再需要一遍又一遍执行相同 join patterns。
- 当 failures 发生时,而且它们一定会发生,debugging 会简单得多,因为 dbt 会准确告诉你哪个 model 导致 failure。如果你将 transformations 拆分为 modular components,就能快速识别 root cause。
- Modularity 增加了一层 abstraction,可以隐藏 data transformations 的复杂性。
- 最重要的是,modularity 提升了 data transformation 的 scalability。需要添加一张新的 fact table?没问题,因为你所需的大多数 staging 和 intermediate models 很可能已经在那里了,因此可以花更多时间向 business 交付价值,而不是担心如何获取所需 data。
这并不是说你不能采用 monolithic data transformations pattern,把某个 fact 或 dimension table 的所有 transformation code 写在单个 .sql file 中。这样可以工作,但应该问问自己,这是否是你和 team 最有效的工作方式。话虽如此,staging-intermediate-marts pattern 并不是实现 modularity 的唯一方式。你也可以实现一种 pattern,不创建与 source tables 一对一的 staging models。事实上,我们见过许多 teams 这样做并取得成功。这个 pattern 相对不那么 modular,但有些 teams 会使用 staging layer 容纳所有 transformation logic,在那里测试 transformations,然后一切检查通过后再 promote 到 mart models。这通常被称为 Write-Audit-Publish(WAP)pattern。我们在第 2 章讨论过它,但值得再次提到,因为 structuring project 有很多不同方式,我们不想让你觉得自己只能被限制在一种方式中。遵循这种 pattern,可以移除维护一个完整 transformation layer 的负担,因为该 layer 只是 source data 的 clean copy。最后,这些 patterns 并不是互斥的,大多数情况下也确实不是。你完全可以构想任何你认为最适合 data、requirements 和 use case 的 pattern。Data 世界中有很多强烈观点,有些很棒,有些并不好。因此,我们鼓励你在判断什么最适合自己时,有意图地做选择。
不过,如果你选择以 modular fashion 实现 models,DAG 会更容易理解,但我们提醒你不要过度 abstract project。为了 ease of use 的 modularity 与让事情过于 confusing 的 modularity 之间有一条细线。我们无法准确告诉你这条线在哪里,因为它很大程度取决于 use case 所需 business logic,但我们提醒你在选择 transformations 的 modular 程度时考虑这一点。
我们之前章节中已经提到过 DAG。再次提醒,它是 dbt 的核心 component,允许 dbt 解释不同 transformations 之间如何相关。DAG(directed acyclic graph)是 lineage graph,展示 dbt project 中所有 objects 之间的关系。虽然 DAG 不是 dbt 独有的概念,但理解你能解释 project 生成的 DAG 的重要性非常关键。
Tip
你可以通过运行 dbt docs generate 和 dbt docs serve 来生成 project 的 DAG。或者,如果使用 dbt Cloud,可以在 cloud IDE 中看到它。
假设我们没有以 modular fashion 编写 transformation,并且已经建模了一张 fact table 和两张 dimension tables。图 4-2 展示了一个 DAG 示例,其中 data 直接从 sources 流向这些 mart models。
图 4-2:一个 messy DAG
虽然这个 DAG 现在还比较容易理解,因为 DAG 末端只有三个 mart models,但在真实世界中,拥有数百甚至数千个 mart models 是很常见的。可以想象,如果每次都直接从 source tables 查询 data,那么很快就会得到一个非常混乱、难以理解的 DAG。不过,我们再次建议考虑 abstraction 的收益与 understandability 的平衡。过度 abstraction 与完全没有 abstraction 一样糟糕,甚至更糟。相反,如果遵循 staging-intermediate-marts pattern,就可以做一些修改,引入更多 modularity,使 DAG 更 scalable 且更容易理解。图 4-3 展示了将其 refactor 得更 modular 后 DAG 的样子。
图 4-3:一个 modular DAG
可以看到,通过在 source tables 和下游其他内容之间添加 staging layer,DAG 开始明显变得更干净。然后,我们引入一个 intermediate model,将 order items 和 products join 在一起。最后,fact orders table 会作为其他 upstream models 的组合被创建,包括 staging、intermediate 和 dimension models。虽然我们没有做太多修改,但添加了足够 modularity,使这个 DAG 更 readable,因为 data flow 的重叠减少了。
Node Selection
我们在第 1 章中讨论过运行 models,并提供了大量可与 commands 一起使用的 command-line flags。现在已经通过示例构建了 models,你可能会想如何运行这些 models。提醒一下,有两个 commands 可以用于将 models materialize 为 database 中的 objects:
dbt run
该 command 会执行 dbt project 中所有 models,并将它们 materialize as database objects。
dbt build
该 command 与 dbt run 做相同事情,但还会将所有 seeds、snapshots 和 tests 纳入 dbt execution。
虽然这些 commands 本身很有用,但常见 pattern 是只想运行单个 model 或 models subset。因此,本节剩余部分将讨论 node selection 是什么,以及如何使用它自定义哪些 nodes 会包含在 dbt invocations 中。当我们说 “nodes” 时,指的是你在 dbt 中定义的大多数 objects,尤其是 sources、seeds、models、snapshots 和 tests。因此,当我们说 node selection 时,指的是修改某个 dbt command,例如 run 或 build,以 include 或 exclude 指定 nodes。
dbt 中的 node selection 极其灵活,但这种灵活性也带来 learning curve,尤其当你开始使用更高级 selectors 时。由于 node selection 有许多不同方式,我们会从基础开始,并提供逐渐增加复杂度的示例。我们不会覆盖所有可能概念,因为数量太多,但到本节结束时,你应该能熟悉 node selection syntax,并具备构建相当复杂 dbt selectors 的知识。我们会覆盖:
- Node selection basics
- Graph and set operators
- Selection methods
- YAML selectors
先看 node selection 的基础。你可能会惊讶,因为本书前面一些示例中已经见过 node selection syntax。最基础形式下,node selection 是在发送给 dbt 的 command 中加入 flag 和 node name。例如,假设你有一个名为 fct_daily_orders 的 model,想对这个 model 运行 dbt run command,而且只运行这个 model。为此,我们会使用 --select command-line flag。完整 command 如下:
dbt run --select fct_daily_orders
但如果你想做相反的事情:运行 project 中除 fct_daily_orders model 之外的所有 models 呢?这时就要使用 --exclude command-line flag。可以将 selection criteria 改写为:
dbt run --exclude fct_daily_orders
这是对 node selection 概念的良好介绍。运行 individual models 在 development environment 中很有用,但现实中,production job 对 node selection 的 requirements 可能复杂得多。也许你需要运行某个 node 下游的所有 nodes、只运行带特定 tag 的 models,或者只运行两个不同 selectors 之间的交集 models。幸运的是,dbt 提供了足够灵活的 node selection,可以单独实现这些 use cases,也可以将它们组合起来。这就引出了 node selection 中下一个、稍微更复杂的概念:graph and set operators。
Tip
本节大多数内容会聚焦于不同 selectors 与 --select flag 结合使用,但你也可以将所有这些与 --exclude flag 组合使用。
Graph operators 用于选择在 lineage 上以某种方式共享关系的 nodes。另一种理解方式是查看 DAG,并沿着 nodes 之间的 arrows 追踪。Graph operators 允许你基于 nodes 在 DAG,也就是 graph 中的连接方式,选择 invocation 中包含的 nodes。最直接的 graph selector 是 plus sign(+)selector。它允许你选择所选 node 的 upstream 或 downstream nodes。例如,假设你想运行 fct_daily_orders model 以及它 upstream 的所有 models。可以这样写 command:
dbt run --select +fct_daily_orders
值得注意的是,plus sign selector 是 fully inclusive 的。也就是说,在前面的示例中,fct_daily_orders model 会被运行,它的 parent(s)、grandparent(s)、great grandparent(s) 等都会被运行,直到没有更多 upstream lineage 可运行。你可能已经猜到,如果想运行自身及其 children、grandchildren 等,也可以将 plus sign selector 放到 model name 后面:
dbt run --select fct_daily_orders+
也许并不是每次都需要运行某个 model 的整个 upstream 或 downstream lineage,而是想限制 node selection 中包含的 ancestry。当然,dbt 有 selector 可用于修改 plus sign selector。可以通过使用 n-plus selector 实现。使用这个 selector 时,只需要根据是想运行 upstream 还是 downstream models,在 plus sign 前或后添加数字。下面提供几个示例,展示 n-plus selector 如何用于 upstream、downstream,以及二者同时选择:
##Run fct_daily_orders and its parent models.
dbt run --select 1+fct_daily_orders
##Run fct_daily_orders, its children, grandchildren, and great grandchildren.
dbt run --select fct_daily_orders+3
##Run fct_daily_orders, its parents, and children.
dbt run --select 1+fct_daily_orders+1
还有其他 graph selectors,例如 at(@)和 star(*)selectors。@ operator 可用于选择一个 model、它的 children,以及它的 children 的 indirect parents。我们承认这里 parent-child analogy 开始有点不太好用了。这个 selector 在某些情况下很有帮助,例如 downstream models 的其他 parents 可能不存在或已 stale,因为该 selector 会确保它们被运行。Syntax 示例:
dbt run --select @fct_daily_orders
最后要覆盖的 graph selector 是 star(*)selector,它概念上非常简单。它用于运行某个特定 directory 或 package 中的所有 nodes。例如,如果想 build file path 为 models/marts/product 内的所有 models 及相关 nodes,可以这样组织 command:
dbt build --select marts.product.*
注意这个 command 的几个点:
- selector 中不需要包含
modelsdirectory。 - Directories 使用 dot notation 分隔,而不是通常的 forward slash。
- 这个示例使用
buildcommand,这对 node selection 来说完全有效。多数情况下,关于 node selection,可以使用run、test、build或seed。
接下来看看 set operators。它们初看可能有点难掌握,不是因为概念复杂,而是 syntax 需要多看几遍才记得住。选择 nodes 时可以使用两类 set operators:union 和 intersection。
Union 的概念相当直接,因为你只需提供两个或更多 selectors,dbt 就会 invoke 你选择的所有 nodes。我们喜欢把 union set operator 看作是多个 dbt commands 的 shorthand。来看一个例子。假设你仍然想运行之前示例中的 fct_daily_orders model 及其 downstream dependencies,但也想运行另一个名为 fct_daily_returns 的 model 及其 downstream dependencies。当然,你可以写两个不同 run commands:
dbt run --select fct_daily_orders+
dbt run --select fct_daily_returns+
但 union operator 允许你将它们简写成一个 command:
dbt run --select fct_daily_orders+ fct_daily_returns+
你可能注意到,在两个 node selectors 之间除了 whitespace 没有其他东西。这就是我们说 syntax 有点怪的原因。fct_daily_orders+ 和 fct_daily_returns+ 之间存在 whitespace,就是 dbt 推断你想 union 这些 selectors 的方式。是否使用 union operator,还是写单独 commands,取决于你。但根据我们的经验,写两个单独 commands 更 declarative 和 clear。唯一差异是它们不会并行运行,一个 command 必须完成后第二个才会运行。这可能会或不会成为问题,但你应该考虑这一点。
第二个 set operator 是 intersection selector,用于运行 selected nodes 以及它们的 common nodes。捕获 intersections 的 syntax 是在 selectors 之间使用 comma(,)。例如,如果我们想运行 +fct_daily_orders selector 和 +fct_daily_returns selector 的交集,可以这样写 command:
dbt run --select +fct_daily_orders,+fct_daily_returns
用 visual 更容易表示这一点。图 4-4 中,你可以通过查看 DAG 中带有 check mark 的 nodes,识别该 command 会运行哪些 models。
图 4-4:使用 intersection syntax 时被运行 nodes 的示例
到目前为止,在几乎所有这些示例中,我们都使用 model names 来选择哪些 nodes 会作为 dbt command 的一部分执行,但其实还有许多其他 selection options。这些被称为 node selection methods,可以使用很多不同 methods。由于选项很多,我们不会详细解释每一个如何工作。相反,我们提供一个 methods 列表,以及每个 method 对应的示例 command。无论使用哪种 method,它们都会遵循 method_name:selector 的 syntax。如果其中任何一个让你感兴趣,建议你尝试使用这些 methods 的不同组合创建 commands:
Tag: dbt run --select tag:finance
Source: dbt run --select source:hubspot
Resource type: dbt build --select resource_type:model
Path: dbt run --select path:models/marts/product
File: dbt run --select file:fct_daily_orders.sql
Fqn: dbt run --select fqn:fct_daily_orders
Package: dbt run --select package:dbt_artifacts
Config: dbt run --select config.materialized:incremental
Config (cont.): dbt run --select config.schema:finance
Test type: dbt test --select test_type:generic
Test name: dbt test --select test_name:not_null
State (more on this in Chapter 10): dbt run --select state:modified
Exposure: dbt run --select exposure:some_important_dashboard
Result (rerun failed models): dbt run --select result:error+ --state target/
Source status: dbt source freshness && dbt run --select source_status:fresher+ --state target/
Group: dbt run --select group:product
Version: dbt run --select version:latest
Access: dbt run --select access:private
最后,但绝不最不重要的是 YAML selectors。它们通过创建一个 YAML file 来列出某个 command 的所有 selection criteria。这为维护 jobs 中使用的 production-grade selectors 提供了一种干净方式。此外,相比将所有 selection criteria 列成一个长字符串,它为你的 selection criteria 提供了一些结构。我们建议,当你有超过两种 selection criteria 时使用 YAML selectors。这个数字有些主观,但我们认为这是转向 YAML selectors 的甜点。当然,实际情况可能因项目而异。
下面是 YAML selectors 的完整 syntax:
selectors:
- name: my_selector
description: This is my YAML selector.
definition:
union | intersection:
- method: some_method
value: nightly
children: true | false
parents: true | false
children_depth: 1
parents_depth: 1
childrens_parents: true | false
indirect_selection: eager | cautious | buildable
- exclude: same syntax as above...
基础 syntax 很好,但我们来看一个实际示例。假设我们想基于三个 criteria 的交集选择 nodes:
- Has a tag of finance
- Is from the source of Quickbooks
- And is configured to be materialized incrementally
为实现这一点,我们的 YAML selector definition 会像这样:
selectors:
- name: incremental_finance
description: This is used to run incremental finance models.
definition:
intersection:
- method: tag
value: finance
parents: true
- method: source
value: quickbooks
parents: true
- method: config.materialized
value: incremental
parents: true
现在这个 YAML selector 已经构建完成,就可以使用 dbt command 运行它。为了包含 tests、seeds 和 snapshots,可以使用 build command:
dbt build --selector incremental_finance
可以看到,node selection 可以很简单,但当你开始使用 set operators 和 YAML selectors 等 features 时,也可以变得非常复杂。和大多数事情一样,我们建议从简单开始,只有当额外 complexity 带来收益,例如更容易 development 或 maintenance 时,再逐渐增加 complexity。
Model Configurations
本章最后,我们想提供一些关于 additional configurations 的信息,这些 configurations 可以应用到 models 上。本章中,我们讨论过使用 config block 在 model level 设置 configurations,或者对 Python models 使用 config method,但这并不是设置 model configurations 的唯一方式。事实上,model configurations 可以设置在三个不同位置,它们共同构成 configuration hierarchy,如图 4-5 所示。
图 4-5:Configuration hierarchy
这说明 dbt 会使用最低层级的 configs。例如,如果你在 dbt_project.yml 和 model 的 .sql file 中都定义了 materialization type,那么 .sql file 中设置的 configuration 会优先生效。
到目前为止,我们已经提供了多个示例,展示如何直接在 model files 中设置 model configurations。现在来看如何在 .yml files 中配置 models。Listing 4-20 提供了一个 model_schema.yml file 如何组织的示例,随后 Listing 4-21 提供了 dbt_project.yml file 如何组织的示例。
version: 2
models:
- name: stg_furniture_mart_products
config:
materialized: view
tags: ['staging']
columns:
- name: ProductID
- name: Product
- name: Price
...
- name: stg_furniture_mart_orders
...
Listing 4-20:schema.yml file 示例
name: 'my_first_dbt_project'
version: '1.0.0'
config-version: 2
profile: 'my_first_dbt_project'
model-paths: ["models"]
...
models:
my_first_dbt_project:
staging:
+materialized: table
intermediate:
+materialized: view
marts:
+materialized: view
...
Listing 4-21:dbt_project.yml file 示例
从这两个示例可以看到,我们以不同方式定义了某个 model 或一组 models 的 materialization type。这个示例中使用的是 materialized configuration,但请记住,大多数 configurations 都可以在 dbt project 的不同位置设置。先看 Listing 4-20,也就是 model_schema.yml file,了解 configurations 如何设置。在这个 file 中,我们逐个命名 models,从而可以提供 configurations。对于 stg_furniture_mart_products model,我们将 materialization 设置为 view。此外,这个 file 也是处理 models documentation 和 testing 的地方,这些内容会在本书后面讨论。
Tip
model_schema.yml 可以命名为任何你想要的名字;我们只是遵循这个 naming convention。只要它是格式正确的 YAML file,dbt 就会知道该 file 的用途。
不过,在 Listing 4-21 中,我们也设置了 materialized configuration,但注意它不是为 individual models 设置的。相反,dbt_project.yml 中的 configurations 应用于 entire directories。我们知道 stg_furniture_mart_products 位于 staging directory 中,因此根据 dbt_project.yml,该 model 应该 materialized as table。可是,如果回忆 configurations 应用的 hierarchy,就能理解 stg_furniture_mart_products model 会 materialized as view,因为:
materializedconfiguration 没有在 model file 中设置。- 因此,dbt 会转向
model_schema.ymlfile,而我们在那里将 configuration 设置为 view。 - 由于 configuration 已经在
model_schema.yml中设置,dbt 会完全忽略dbt_project.yml中设置的 configuration。
到这里,你可能会想还有哪些 configurations 可用。答案是很多!不过,需要知道有些 general configurations 适用于所有 dbt adapters,也有些 adapter-specific configurations 是为你的 data platform 定制的。为了简单起见,我们这里只覆盖 general configurations,但鼓励你查阅 dbt documentation,了解你正在使用的 adapter 还有哪些 additional configurations 可用。
alias
Overrides relation 的 naming。
database
指定 model 或 seed 的 target database。
enabled
设置为 false 时,可以禁用 models、seeds、snapshots 和 tests。
full_refresh
设置 model 总是或从不 fully refresh。Full refresh 指 incremental model 从头重建。
contract
强制 model 与 YAML files 中定义的 name、data type 和其他 attributes 完全匹配。
grants
指定 runtime 时应应用到 models、seeds 和 snapshots 上的 grants。
group
允许限制从其他 downstream models 引用 private models 的访问。
docs
从 dbt-generated documentation 中隐藏 node。注意,该 node 仍会出现在 project DAG 中。
persist_docs
将 YAML 中定义的 documentation 持久化为 database 中 column 和 relation comments。
pre-hook
在 model run 之前运行一个或多个 ad hoc SQL statements。
post-hook
在 model run 之后运行一个或多个 ad hoc SQL statements。
schema
指定 model 或 seed 的 target schema。如果不想将所有 models 构建到 default target schema 中,这很有用。
tags
在 nodes 上创建一个或多个 tags。用于 grouping nodes,也可在 CLI commands 中作为 selector 使用。
meta
创建一个 arbitrary metadata dictionary,存储到 dbt 生成的 manifest.json 中。这个 metadata 会在 documentation site 中可见。
Summary
本章中,我们介绍了 dbt 最基础的组成部分:models。没有 models,dbt 的其他部分几乎没有价值。但正如你会在后续章节看到的,models 可以衍生出许多内容:
- Other models
- Tests
- Documentation
我们首先讨论 SQL models。很长一段时间,SQL models 是 dbt 中唯一可以构建的 model 类型。本章中,我们覆盖了 SQL models 可用的不同 materializations。Materialization 定义 dbt 会如何将 model 持久化,或者不持久化到 database 中。SQL models 可以使用四种 materializations:
View
在 database 中持久化一个 view。通常作为 staging models 或其他 lightweight transformations 的 preferred materialization type。
Table
将 model 返回的数据持久化为 database 中的一张 table。通常用于 mart models,也就是 facts 和 dimensions,或其他需要快速返回 query results 的场景。
Incremental
与 table materialization 目的相同,但允许你 incrementally load table,而不是每次 invoke dbt 都重新构建。当 table materialization 无法提供所需 performance 时最有用。
Ephemeral
一种 unique materialization,不会在 database 中持久化。相反,它返回一个 CTE,可在 downstream dbt models 中被引用。
在 SQL models 的讨论之后,我们讨论了新成员:Python models。截至本书写作时,Python models 仍处于早期阶段,所以我们没有做非常详细的深入介绍。但我们讨论了基础内容,包括它们从根本上如何工作、如何将 data 持久化为 database 中的 tables,以及如何使用传递给 Python models 的 dbt class methods。此外,我们也简要讨论了什么时候最适合使用这些 models。我们的观点是,如果可以,models 应该使用 SQL 构建;但 Python 为 additional complex workflows 打开了大门,包括 data science 和 machine learning。过去这些 data professionals 必须在 dbt 之外工作,但现在在合理时可以直接在 dbt 中工作。
虽然理解如何构建 models 很重要,但我们也见过一些 dbt projects 因缺乏准备和没有充分利用 modularity,很快变成无法维护的混乱。通过以 modular fashion 编写 transformations,project 会更易维护,因为 common transformations 更容易复用,debug failures 更简单,DAG 也更容易理解。我们鼓励你遵循 staging、intermediate 和 mart models 的构建模式,使 transformations 始终保持 modular。
下一章中,我们将讨论 dbt 中另一种可以使用的 resource type:snapshots。Snapshots 是一种在没有更好方式时,生成 type 2 dimensions 以跟踪 source data historical changes 的方法。我们会提供构建 snapshots 的示例,并说明何时以及为什么可能需要使用它们。