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 的依赖链,以及不同物化方式在链路中的位置。

dbt 模型依赖链与物化方式示意 source raw_orders / raw_customers stg_orders 物化:view stg_customers 物化:view int_order_items 物化:ephemeral orders_enriched 物化:table fct_order_events 物化:incremental ref() ref() ref() ref() ref() ref() 图例说明 view:轻量视图,不存数据 ephemeral:CTE 内联,无对象 table:全量重建物理表 incremental:增量写入

如何选择物化方式

选择物化方式时,可以从三个维度判断:数据量、查询模式和刷新成本。

  • 数据量小、逻辑常变:优先 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 等,具体取决于适配器支持。

注意:incremental 模型如果增量条件写错,可能导致数据重复或遗漏。建议在开发环境先用全量结果与增量结果做对比校验,确认行数和关键指标一致后再上线。

ref 函数带来的工程收益

使用 ref() 而不是硬编码表名,带来三个直接收益:第一,dbt 自动构建 DAG,按依赖顺序执行;第二,环境切换时自动指向正确的 schema;第三,dbt docs generate 可以生成血缘图,帮助排查影响范围。当你要修改一个上游模型时,通过 dbt ls --select +orders_enriched 就能快速列出所有下游依赖。

模型定义、物化方式和 ref 依赖,构成了 dbt 项目最基本的运行单元。掌握这三者之间的关系,后续的测试、文档、快照和 exposures 才有稳定的落脚点。