必须掌握的 dbt 概念
0. 数据仓库
您需要了解的前置概念之一是数据仓库。仓库用于存放某家公司所有的数据。
公司之所以构建数据仓库,是因为它们支持分析以及一切与数据(咳咳,结构化数据)有关的工作。仓库存储按表组织的历史数据,其结构经过优化,便于快速查询与分析。
实现数据仓库的工具有很多:
- PostgreSQL
- MySQL
- Snowflake
- BigQuery
- Redshift
等等。
dbt 不负责将数据收集或加载到上述工具中,而是用于在其中对数据进行转换。换言之,它完成的是所有数据仓库核心流程 ETL/ELT(抽取、转换、加载)中的 T。
1. dbt Core 与 dbt Cloud
dbt 提供两种使用方式:dbt Core 和 dbt Cloud。
dbt Core 是一个开源库,涵盖了 dbt 的大部分功能。它提供命令行界面(您会逐渐爱上的 dbt 命令),可用于管理项目中的数据转换。
dbt Cloud 是面向团队的企业级方案。除了 CLI,dbt Cloud 还提供更友好的基于 Web 的 IDE。借助它,您就不必过多操心数据库连接和编辑 YAML 文件(正如您将在后文看到的)。
dbt Cloud 还提供作业调度、高级集成和高优先级支持等附加功能。
下表总结了 dbt Core 与 dbt Cloud 的差异:

尽管有这些附加功能,本文将以 dbt Core 为主,因为它最适合本地项目、测试与学习。您可以在任意操作系统上通过 pip 安装它(当然要在虚拟环境中)。
我将使用 Conda 环境:
$ conda create -n learn_dbt -y
$ pip install dbt-<adapter_name>
您需要将 adapter_name 替换为您要使用的数据库。dbt 背后的公司 dbt Labs 已为不同数据平台集成了许多适配器。
本文将使用 dbt-duckdb 适配器连接 DuckDB 数据库。但您也可以使用dbt 文档此页列出的任一适配器。
$ pip install dbt-duckdb
至此,初始设置完成!
2. dbt 项目
简单来说,dbt 项目是在您的机器上包含对数据执行转换所需一切内容的目录。它包含许多 .sql 文件(称为模型)以及用于配置的 YAML 文件。
要创建 dbt 项目,您可以在 CLI 中使用 dbt init <project_name> 命令:
$ dbt init dbt_learn
终端会提示您输入与可用数据平台适配器对应的代码。由于这里只有 DuckDB,您可以按 1。
$ cd dbt_learn
在 dbt_learn 中,您将看到如下结构:

正是在这里,数据工程师开始更像软件工程师,因为 dbt 项目让您能够:
- 有序且模块化:将数据转换保持有序,并拆分为易于管理的单元,使代码更易理解与维护。
- 版本控制:跟踪更改并回滚到模型的先前版本,确保数据一致性与可复现性。
- 协作:允许多人在同一项目中按角色与权限协同工作。
- 可测试:为模型编写测试,确保其按预期工作,并在部署至生产前发现潜在问题。
- 可复用:跨不同数据源与环境复用同一项目,实现一致的数据转换。
简而言之,dbt 项目为管理与编排数据转换提供了强大的方式,将软件工程期待已久的优势带入数据领域。
3. dbt 项目配置文件(Profiles)
我们已经初始化了一个 dbt 项目,现在需要连接到一个现有数据库(或从零创建一个)。为此,我们需要一种安全方式,将数据库凭据提供给 dbt 以建立连接。这就是我们要使用项目配置文件(profile)的原因。
项目配置文件是一个 YAML 文件,包含所选数据平台的连接信息。该文件位于 $HOME 下的 .dbt 目录中,名为 profiles.yml。目前它看起来是这样的:

该文件为我们的项目列出了一个名为 dbt_learn 的配置文件。它指定了两个输出:dev 和 prod。
输出是单独的配置,用于指定连接到不同数据仓库或数据库的方式。通过输出,您可以管理与不同环境的连接:
- 开发
- 测试
- 生产,等等。
在我们的配置文件中,默认输出为 dev,它在 target 字段中列出。您可以根据需要将其更改为其他输出。现在我们保持不变。
注意:只要在其余 dbt 文件中引用正确,您可以更改 profile 与 output 的名称。
path 字段指定了一个名为 dev.duckdb 的现有数据库的位置。若不存在,dbt 的 DuckDB 适配器会在我们的工作目录(dbt_learn 项目内;path 字段是相对项目目录的相对路径)中创建它。由于我们尚无名为 dev.duckdb 的数据库,将通过运行 dbt debug 让 dbt 创建它。
$ dbt debug
debug 子命令用于测试项目的多方面内容,例如:
profiles.yml文件中的错误profiles.yml中的数据库连接信息- 数据库适配器
dbt_project.yml文件中的错误,等等。
如果您看到绿色的“All tests passed”消息,并出现了新的 dev.duckdb 数据库,那么就一切顺利。
4. dbt 模型
模型是 dbt 的核心,它们代表了 dbt 所擅长的实际数据转换。
数据模型是一种概念,用于表示某组数据的结构与关系。dbt 中的数据模型更为简洁与具体,具有以下特征:
- 表示一次数据转换(例如执行某个清洗操作)
- 通常使用 SQL 写在
.sql文件中(较新版本的 dbt 也支持 Python) - 通常只包含一个 SELECT 查询
我们的 dbt_learn 项目在 models/example 目录下自带两个示例模型:

我们将删除它们并创建自己的模型:
$ rm -rf models/example
$ mkdir models/stats
$ touch models/stats/average_diamond_price_per_group.sql
上面代码的最后一行在 stats 目录中创建了一个名为 average_diamond_price_per_group 的模型。为模型命名时务必具备描述性。
在该模型(.sql 文件)内,粘贴这段用于测试的占位 SQL 查询:
SELECT 1 AS Id
然后用 dbt run 运行该模型:
$ dbt run
您应当会看到绿色的“Completed successfully”消息。
完成以上设置后,我们可以把一些数据加载到 dev.duckdb 数据库中。我们将使用 parquet 文件,因为 DuckDB 原生支持它。
SELECT AVG(price), cut
FROM "diamonds.parquet"
GROUP BY cut
该查询应返回同样的成功消息。
在本教程中,我已将 diamonds 数据集准备为 parquet 文件。您可以在这个 GitHub gist 中找到下载到您工作区的脚本片段。
我们刚刚通过一个返回数据集汇总统计的 SELECT 语句,创建了第一个 dbt 模型。实际工作中,您的模型取决于业务需求以及不同同事如何使用数据库。因此,我们不会聚焦于模型的具体业务逻辑,而是关注如何正确实现它们。
5. dbt 中的 DAG
在真实项目中,您的模型很可能彼此依赖,形成某种层次结构。在数据领域,这种层次结构称为有向无环图(DAG)或血缘图。
一张 DAG 就能起到“千言万语”的文档效果。看看来自dbt DAGs 页面的这个示例:

图中共有四个模型,均线性连接到其下游模型。stg_users 和 stg_user_groups 是 int_users 的父模型,而后者与上游的 stg_orgs 一起,成为 dim_users 的父模型。
注意:upstream(上游) 与 downstream(下游) 常用于指代模型在 DAG 中的相对位置。
DAG 的一个关键点是不存在闭环。这意味着下游模型(由更早的模型产出)不能与上游模型相互连接,这也是“无环”一词的由来。
除了可视化上的直观性,DAG 的作用还在于让 dbt 能按依赖关系构建/更新模型。若不为上述四个模型定义 DAG,dbt 将按字母顺序构建它们,这会导致各种红色报错信息。
在 dbt 中,我们将使用 Jinja 模板来定义 DAG。
6. dbt 中的 Jinja 模板
在上面的 DAG 中,int_users 模型由 stg_users 与 stg_user_groups 生成。我们需要在项目中指定这种关系,否则 dbt run 会按字母顺序执行所有模型,这意味着 int_users 会最先执行。由于其依赖尚未物化,这将导致报错。
目前,int_users 模型可能长这样:
SELECT some_column
FROM stg_users as su
JOIN stg_user_groups as sug
ON su.a = sug.a
现在,我们用 Jinja 将这三个模型连成 DAG 的节点:
SELECT some_column
FROM {{ ref("stg_users") }} as su
JOIN {{ ref("stg_user_groups" )}} as sug
ON su.a = sug.a
我们不再直接写模型名,而是把它们放进名为 ref 的 Jinja 函数。其语法为 {{ ref("column_name") }}(注意空格与引号)。当查询被编译时,Jinja 函数会被替换为实际的模型名。
请注意,stg_users 与 stg_user_groups 应当作为 .sql 文件存在于您的 dbt 项目中。
现在,当我们执行 dbt run 时,dbt 会查找每个模型的依赖,建立连接并按依赖顺序执行。
ref 并不是 dbt 中唯一可用的 Jinja 函数。实际上,借助 Jinja 的其他函数和特性,您可以显著扩展 SQL 语句的功能。以下是一些示例:
- 在模型文件中使用 Jinja 创建变量:
{% set status = 'active' %} -- Define a variable
SELECT *
FROM customers
WHERE status = {{ status }};
- 在模型配置文件中通过 config 对象定义变量。
如果我们有 models/model_properties.yml 文件,且包含以下字段:
# models/model_properties.yml
version: 2
models:
- name: my_model
config:
target_schema: analytics
我们可以在任意 .sql 文件中用 Jinja 访问其字段:
{% set target_schema = config.target_schema %}
CREATE TABLE {{ target_schema }}.{{ target_table }} AS
...
- 使用条件与循环(这点相当惊喜!):
条件:
{% if some_condition %}
SELECT * FROM test_data
{% else %}
SELECT * FROM production_data
{% endif %}
循环:
SELECT
order_id,
{% for payment_method in ["bank_transfer", "credit_card", "gift_card"] %}
SUM(CASE WHEN payment_method = '{{ payment_method }}' THEN amount END) AS {{ payment_method }}_amount,
{% endfor %}
SUM(amount) AS total_amount
FROM {{ ref('raw_payments') }}
GROUP BY 1;
- 通过 Jinja 在 SQL 中创建函数(宏,没错,真的可以):
宏如下:
{% macro create_table(table_name, columns) %}
CREATE TABLE {{ table_name }} (
{% for column in columns %}
{{ column.name }} {{ column.type }},
{% endfor %}
);
{% endmacro %}
在模型中使用方式如下:
{% call create_table('my_customer_table', [
{'name': 'id', 'type': 'integer'},
{'name': 'name', 'type': 'varchar(255)'},
{'name': 'email', 'type': 'varchar(255)'},
]) %}
INSERT INTO {{ my_customer_table }} (id, name, email)
SELECT customer_id, customer_name, customer_email
FROM raw_customers;
若想进一步学习在 dbt 中配合 SQL 使用 Jinja,请查看 dbt 文档此页面。
7. dbt 测试
优秀的软件开发者深知需要不断为代码编写测试以发现缺陷与错误。既然 dbt 让数据工程师更像软件开发者,它也提供了简明的测试工作流,既有内置测试,也支持自定义测试。
目前,dbt 提供以下四种内置测试:
unique—— 验证所有值是否唯一not_null—— 检查是否存在缺失accepted_values—— 验证所有值是否在指定列表内,带有values参数relationships—— 验证与特定表或列的关系,带有to与field参数
要指定对哪些列应用哪些测试,我们会在 models 目录下使用一个名为 model_properties.yml 的 YAML 文件。
注意:模型运行并不强制需要 model_properties.yml,它可以命名为任意名称。但如果您希望通过测试来校验流入模型的数据,该文件必不可少。
下面看看如何使用 not_null 测试检查 diamonds 表中 cut 列的缺失值。首先,在 models 目录下创建 model_properties.yml 文件:
$ touch models/model_properties.yml
在文件中粘贴以下内容:
version: 2
models:
- name: average_diamond_price_per_group
columns:
- name: cut
tests:
- not_null
在 models 下的 - name 字段中,我们指定了要为哪个模型定义属性。然后指定列及其名称。最后,在 tests 字段下列出 not_null 测试。
现在,您可以在运行 dbt run 之前,用该测试执行数据校验。命令为 dbt test:
$ dbt test
如果收到错误消息,则说明测试失败,您需要检查表中的问题,并在必要时修复。
一个可供参考的典型 dbt 工作流
要在项目中成功使用 dbt,您可以遵循以下推荐流程:
1. 项目初始化
- 安装
dbt并用dbt init创建新项目
2. 配置
- 为项目选择数据库平台
- 在主目录下的
profiles.yml中配置数据库凭据。 - 调整项目设置:修改
dbt_project.yml以进行项目级设置(如版本、依赖)。
3. 开发
- 编写模型 SQL 代码:在
models目录中创建.sql文件用于定义模型。 - 编写模型测试:在
tests目录中创建.yml文件用于定义测试。 - 增量测试:在开发过程中频繁使用
dbt test运行测试。 - 调试问题:使用
dbt debug进行排查。
4. 本地验证(本文未覆盖的主题)
- 构建项目:使用 dbt build 编译模型与测试。
- 完整测试:使用 dbt test 执行所有测试。
其他最佳实践:
- 版本控制:使用 Git 进行协作与版本控制(这是必需的)。
- 文档化:在模型与测试中撰写可读性强的注释。稍后可使用
dbt docs generate在 Web 服务器上渲染模型文档(是的,做得到)。 - 性能剖析:利用 dbt 的剖析能力发现并修复性能瓶颈。
- 持续集成(CI):将 dbt 集成到 CI/CD 流水线中,实现自动化测试与部署。
结语与更多资源
本教程覆盖了许多基础内容,但正如开头所说,dbt 是一款功能繁多的大型工具。要熟练到能在生产环境中自如运用,仍需一段时间。为何不利用以下资源,加速实现这一目标?

我是一名数据科学内容创作者,拥有超过 2 年的经验,并在 Medium 上拥有较大的读者群。我喜欢用略带讽刺的笔调撰写有关 AI 和机器学习的深度文章——毕竟总得想点办法让这些内容不那么枯燥。我已发表 130 多篇文章,并制作了一门 DataCamp 课程,另有一门正在筹备中。我的内容已被超过 500 万人次阅读,其中有 2 万人在 Medium 和 LinkedIn 上成为关注者。
什么是 dbt,为什么重要?
过去几年,数据科学社区正逐步接受以数据为中心的范式。我们不再一味追求越来越复杂的机器学习模型,而是终于把重心更多放在数据质量上。这也催生了数据工程师的巨大热度,他们如今的薪资水平可与资深数据科学家或 ML 工程师比肩。
而显著提升数据工程师工作体验的一款工具,就是 dbt(data build tool)。其目标是将久经考验的软件工程最佳实践引入数据工程领域,并尽可能快速、简便地产出数据价值。
本文将为初级数据工程师讲解 dbt 的基础知识,帮助您把这一不可或缺的工具加入技能库。您也可以查看我们的《dbt 入门》课程,进一步学习这款强大的工具。
前置要求
本文只需要以下少量准备:
如果暂时达不到这些条件,但您的老板(或您自己)仍要求您学习 dbt,可以使用以下资源:
本 dbt 指南将涵盖什么?
开源社区非常热爱 dbt,他们已将其与几乎所有与数据相关的工具打通。结果如何?文档庞大到即便是快速入门指南,其篇幅也超过了许多完整的 Python 库文档。
因此,本文的目标是以适度的技术深度,向您介绍 dbt 的七个核心概念。完成本教程后,您可以打开 dbt 文档的任意页面,基本都能看懂其在讲什么。
现在,让我们开始吧!