4. dbt模型基础与物化方式
在 dbt 中,一个模型(model)本质上就是一段 SELECT 语句,通常保存在 models/ 目录下的 .sql 文件里。dbt 负责把这段查询编译、包装成具体的数据库对象,并按照依赖关系决定执行顺序。理解模型与物化方式,是掌握 dbt 工程化建模的起点。
模型是什么:从一段 SELECT 到一张表
假设我们要在电商场景中构建一个订单宽表,文件名为 models/marts/orders_enriched.sql,内容如下:
-- models/marts/orders_enriched.sql
select
o.order_id,
o.customer_id,
c.customer_name,
o.order_date,
o.amount,
o.status
from {{ ref('stg_orders') }} as o
left join {{ ref('stg_customers') }} as c
on o.customer_id = c.customer_id
where o.status != 'cancelled'
这段 SQL 本身并不包含 CREATE TABLE 或 CREATE VIEW。dbt 会根据该模型配置的物化方式,自动生成对应的 DDL/DML。也就是说,模型定义的是“数据应该长什么样”,物化方式决定的是“这份数据以什么形式存在于数仓中”。
四种核心物化方式
dbt 内置了四种常用物化方式:view、table、ephemeral 和 incremental。它们各有适用场景,选择时主要看数据量、查询频率、刷新成本和依赖复杂度。
| 物化方式 | 数据库对象 | 每次运行行为 | 典型场景 |
|---|---|---|---|
view |
视图 | 重建视图定义,不存储数据 | 轻量转换、逻辑频繁调整、下游直接查询 |
table |
物理表 | 删除并重建整张表 | 数据量中等、查询频繁、需要稳定性能 |
ephemeral |
无独立对象 | 作为 CTE 内联到下游模型 | 中间逻辑复用、不希望产生额外对象 |
incremental |
物理表 | 首次全量,后续按条件增量写入 | 大数据量事实表、日志表、事件流 |
view 是最轻量的物化方式。它不存储数据,每次查询时实时执行底层 SQL。优点是逻辑变更后无需回刷数据,缺点是复杂查询会拖慢下游。适合放在 staging 层或逻辑尚未稳定的探索阶段。
table 会把查询结果物化成一张物理表。每次 dbt run 时,dbt 会重建这张表。它适合数据量不大但被频繁查询的维度表或汇总表。缺点是全量重建成本随数据量增长而上升。
ephemeral 比较特殊:它不会在数据库中创建任何对象,而是把模型 SQL 作为 CTE 注入到所有引用它的下游模型中。适合做轻量的中间计算,但要注意,如果被多个下游引用,逻辑会被重复展开,可能造成编译膨胀。
incremental 用于处理大数据量场景。首次运行时全量构建,之后每次运行只处理新增或变更的数据。它需要配合 is_incremental() 宏和唯一键策略使用,是 dbt 中最需要谨慎配置的物化方式。
模型依赖与 ref 函数
dbt 最强大的能力之一,是自动解析模型之间的依赖关系。这个能力来自 ref() 函数。当你在模型 A 中写 {{ ref('model_b') }},dbt 就知道 A 依赖 B,并据此构建 DAG(有向无环图),决定执行顺序。
ref() 不仅解决顺序问题,还负责正确的 schema 和数据库限定。编译后,它会展开为完整的表名或视图名,例如 "analytics"."dbt_dev"."stg_orders"。这样模型代码不需要硬编码物理路径,环境切换时也能自动适配。
与 ref() 相对的是 source(),用于引用原始数据源。两者共同构成 dbt 的数据血缘基础。下面这张图展示了从 source 到 staging、再到 marts 的依赖链,以及不同物化方式在链路中的位置。
如何选择物化方式
选择物化方式时,可以从三个维度判断:数据量、查询模式和刷新成本。
- 数据量小、逻辑常变:优先
view,避免每次改逻辑都要回刷数据。 - 数据量中等、下游查询频繁:用
table,用存储换查询性能。 - 纯中间逻辑、不想暴露对象:用
ephemeral,但注意不要被过多下游引用。 - 数据量大、只追加或按时间更新:用
incremental,配合唯一键和增量条件。
一个常见的分层策略是:staging 层用 view,marts 层用 table,大事实表用 incremental,少量中间逻辑用 ephemeral。这不是硬性规定,而是根据团队数据规模和查询习惯逐步调整的起点。
incremental 的关键配置
incremental 模型需要显式告诉 dbt 如何判断增量。最常见的方式是使用 is_incremental() 宏:
-- models/marts/fct_order_events.sql
{{ config(
materialized='incremental',
unique_key='event_id',
incremental_strategy='merge'
) }}
select
event_id,
order_id,
event_type,
event_time
from {{ ref('stg_order_events') }}
{% if is_incremental() %}
where event_time > (select max(event_time) from {{ this }})
{% endif %}
首次运行时,is_incremental() 返回 false,执行全量查询;后续运行时返回 true,只处理 event_time 大于当前表最大时间的数据。unique_key 用于 merge 策略下的更新匹配,incremental_strategy 可选 merge、delete+insert、append 等,具体取决于适配器支持。
ref 函数带来的工程收益
使用 ref() 而不是硬编码表名,带来三个直接收益:第一,dbt 自动构建 DAG,按依赖顺序执行;第二,环境切换时自动指向正确的 schema;第三,dbt docs generate 可以生成血缘图,帮助排查影响范围。当你要修改一个上游模型时,通过 dbt ls --select +orders_enriched 就能快速列出所有下游依赖。
模型定义、物化方式和 ref 依赖,构成了 dbt 项目最基本的运行单元。掌握这三者之间的关系,后续的测试、文档、快照和 exposures 才有稳定的落脚点。