dbt 数据建模:ELT 中的转换层
dbt 数据建模:ELT 中的转换层完全指南
引言:为什么 dbt 成为现代数据仓库的转换标准
在当今的数据分析领域,数据建模早已不是传统 ETL 工具中“先提取再转换”的单一流程。随着云数据仓库(如 Snowflake、BigQuery、Redshift)的崛起,ELT(提取、加载、转换) 模式成为主流。在 ELT 中,原始数据直接加载到目标仓库,随后再由团队使用 SQL 进行转换。
而 dbt(data build tool) 正是这一转换层的核心工具。它让分析师和工程师能够用大家都会的 SQL,通过软件工程实践(版本控制、测试、文档)构建和维护数据模型。本教程将带你从零开始,理解 dbt 数据建模的核心概念、项目结构、建模方式与最佳实践。
dbt 的数据建模哲学:从原始数据到业务逻辑
dbt 在 ELT 流程中的角色
在 ELT 架构中,dbt 只负责 T(转换)。它连接到你已加载好数据的仓库,执行你编写的 SQL 模型,将底层表逐步转化为面向分析的干净、聚合的数据集。
dbt 最强大的地方在于它带来了 数据即代码 的理念:
- 你的数据模型存放在 Git 仓库中,可协作、可审查。
- 每次运行都会生成文档(dbt docs)和测试结果(dbt test)。
- 自动处理依赖关系,确保父模型先运行。
建模的核心:SELECT 语句的模块化
dbt 模型本质上就是 .sql 文件中的一条 SELECT 语句。但这些 SELECT 可以相互引用,形成有向无环图(DAG)。
示例模型 customer_orders.sql:
WITH customers AS (
SELECT * FROM {{ ref('stg_customers') }}
),
orders AS (
SELECT * FROM {{ ref('stg_orders') }}
)
SELECT
c.customer_id,
c.name,
COUNT(o.order_id) AS order_count,
SUM(o.amount) AS total_revenue
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY 1, 2
其中 {{ ref('...') }} 是 dbt 内置的依赖引用,dbt 会自动解析成正确的表名,并构建执行顺序。
搭建 dbt 项目的基础结构
创建一个 dbt 项目
安装 dbt(以 dbt-core + adaptor 为例,如 dbt-snowflake)后,使用命令初始化:
dbt init my_project
这会生成如下核心目录结构:
my_project/
├── models/ # 存放SQL模型
├── tests/ # 自定义测试(可选)
├── macros/ # 可复用的Jinja宏
├── analysis/ # 探索性查询(不构建到仓库)
├── seeds/ # CSV种子数据(按需)
├── dbt_project.yml
└── profiles.yml # 连接配置(通常在 ~/.dbt/ 下)
配置 dbt_project.yml
该文件定义项目名称、版本、模型物化策略等:
name: my_project
version: 1.0.0
profile: default
models:
my_project:
staging:
materialized: view
marts:
materialized: table
这里可以按文件夹设置不同的物化方式,例如临时区使用视图,最终分析层使用表以提高查询性能。
理解物化策略
dbt 提供多种物化方式:
- view:每次查询时动态生成,节省存储但查询可能较慢。
- table:物理创建表,查询快但占用存储,适合最终暴露给 BI 的表。
- incremental:仅更新新数据,极大提升大表转换性能。
- ephemeral:不建表,相当于公用表表达式(CTE),用于中间逻辑复用。
选择合适的物化策略是数据建模性能优化的关键一步。
核心建模技术:分层架构与依赖管理
推荐的分层建模方法
优秀的 dbt 项目通常采用 源层 → 准备层 → 业务层 的清晰结构:
- Sources(源表):在
models/sources.yml声明仓库中已存在的原始表。 - Staging(准备层):对源表进行最小化清洗,如重命名列、转换类型,保持一对一关系,命名为
stg_。 - Intermediate(中间层,可选):复杂逻辑的分解体,通常以
int_开头。 - Marts(业务层):面向业务领域的最终宽表或聚合表,如
fct_sales、dim_customers。
声明数据源(Sources)
在 models/staging/sources.yml:
version: 2
sources:
- name: raw
database: raw_db
schema: public
tables:
- name: orders
- name: customers
之后在模型中用 {{ source('raw', 'orders') }} 引用,带来两个好处:
- 自动生成源表血缘文档。
- 可通过
dbt source freshness检测数据新鲜度。
编写 Staging 模型
例如 models/staging/stg_orders.sql:
SELECT
id AS order_id,
user_id AS customer_id,
CAST(order_date AS date) AS order_date,
COALESCE(amount, 0) AS amount
FROM {{ source('raw', 'orders') }}
这个模型做了干净的重命名和类型转换,成为下游模型唯一依赖的基础表,实现代码复用与一致性。
构建 Marts 模型(星型或宽表模式)
以 Kimball 星型模型为例,可以创建维度和事实表:
dim_customers.sql:客户维度,包含所有客户属性。fct_orders.sql:订单事实表,包含外键和度量值。
举例 dim_customers.sql:
SELECT
customer_id,
name,
email,
first_order_date
FROM {{ ref('stg_customers') }}
fct_orders.sql:
SELECT
order_id,
order_date,
customer_id,
product_id,
amount
FROM {{ ref('stg_orders') }}
通过 ref() 形成依赖链,dbt 运行时会按正确顺序执行,并支持并行。
数据测试与文档:保障模型质量
内置测试
dbt 提供简单但强大的测试类型,可直接在 schema.yml 中配置:
- unique:确保列值唯一。
- not_null:确保列无空值。
- accepted_values:限定列值在枚举列表中。
- relationships:确保外键引用完整性。
示例 models/marts/schema.yml:
version: 2
models:
- name: dim_customers
columns:
- name: customer_id
tests:
- unique
- not_null
- name: email
tests:
- not_null
- name: fct_orders
columns:
- name: customer_id
tests:
- not_null
- relationships:
to: ref('dim_customers')
field: customer_id
运行 dbt test 即可对所有模型执行这些断言,不合格的会自动报错。
自定义测试
你可以编写 SQL 文件放在 tests/ 目录下,返回任何不满足条件的行。例如测试订单金额不能为负数:
-- tests/assert_positive_amount.sql
SELECT order_id, amount
FROM {{ ref('fct_orders') }}
WHERE amount < 0
如果该查询返回任何行,测试失败。
文档与血统图
在模型或列的 .yml 中添加 description 字段,然后执行:
dbt docs generate
dbt docs serve
即可在本地浏览器查看交互式的项目文档,包含表说明、列描述、完整 DAG 血缘线图。这是数据团队沟通和新人上手的利器。
进阶建模技巧:宏、增量与快照
使用 Jinja 宏(Macros)
宏是可复用的 SQL 片段,类似于函数。dbt 自带许多宏,如生成日期数列。你可以在 macros/ 目录下自定义宏。
例如创建一个 clean_phone 宏:
{% macro clean_phone(phone_column) %}
regexp_replace({{ phone_column }}, '[^0-9]', '', 'g')
{% endmacro %}
在模型中使用:
SELECT
customer_id,
{{ clean_phone('phone') }} AS phone_clean
FROM {{ ref('stg_customers') }}
增量模型(Incremental Models)
对于超大表,每次全量重建代价高昂。可以在模型顶部加入条件判断:
{{
config(
materialized='incremental',
unique_key='order_id'
)
}}
SELECT ...
FROM {{ ref('stg_orders') }}
{% if is_incremental() %}
WHERE order_date > (SELECT MAX(order_date) FROM {{ this }})
{% endif %}
is_incremental() 在非首次运行时为 True,实现对增量数据的处理。务必指定 unique_key 以支持合并更新。
数据快照(Snapshots)
快照用于追踪缓慢变化维度(SCD),即数据的历史状态。例如客户地址的变化:
{% snapshot customer_snapshot %}
{{
config(
target_schema='snapshots',
unique_key='customer_id',
strategy='check',
check_cols=['address']
)
}}
SELECT * FROM {{ ref('stg_customers') }}
{% endsnapshot %}
每次运行,dbt 会对比 check_cols 指定的列,如有变化则插入新行记录历史,并加上 dbt_valid_from 和 dbt_valid_to 字段。
部署与生产环境最佳实践
使用 dbt Cloud 或 CI/CD 流水线
本地开发只需 dbt run,但在生产环境中,通常集成 dbt Cloud 或使用 GitHub Actions、Airflow 等编排调度。关键步骤:
- 代码合并到主分支后,触发 CI 流程。
- 运行
dbt test确保无数据问题。 - 运行
dbt run刷新生产模型。 - 生成并发布文档。
环境区分
使用 target 变量区分开发与生产环境,在 profiles.yml 中配置不同的连接。生产目标设为 prod,避免误操作。
模型监控与报警
可以结合 dbt 元数据(如 dbt build 的结果)将任务状态发送到 Slack 或 Datadog,实时掌握数据流水线健康度。
总结:dbt 建模的未来趋势
dbt 不仅是一个 SQL 执行器,更是数据分析工程化的催化剂。通过模块化建模、分层设计、自动化测试和清晰的文档,团队能够以更低的成本维护高质量的数据仓库。
随着 dbt Mesh、语义层 等概念的引入,dbt 正从单纯的转换层向全链路的数据治理平台演进。掌握本教程介绍的建模基础,将为你深入数据工程与分析的职业道路打下坚实根基。
立即动手,用 dbt init 开始你的第一个模型吧!