未记录事实核验 · 状态:维护中

记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。

warning 适用边界与维护说明

本文适合已有 dbt Model 基础的读者练习 Jinja 模板展开、宏封装、变量读取、支付方式聚合、dbt-utils 使用及环境过滤,不是经过运行验证的完整项目。本次仅作全文结构审阅,未进行外部事实核验或安装、编译与执行测试;packages.yml 中 dbt-labs/dbt_date 的包坐标及版本区间仍缺少可确认其可安装性的依据,不能将整份清单视为已验证的安装配置。日期维度、时间过滤、类型转换和哈希函数需按目标数据库适配;自定义代理键未处理 NULL 和分隔符歧义,不宜直接作为生产唯一键方案。正文 prod 示例仅处理 2020-01-01 起的数据,完成全量处理练习时应调整或移除该过滤条件;掌握检查中的近 7 天要求也需自行调整示例的 30 天窗口。

dbt Jinja 与宏 - 让 SQL 具备编程能力

abstract 本文核心

SQL 主要用于声明式查询,本身支持函数;变量与过程控制能力因数据库及其过程语言而异。Jinja 在 dbt 编译阶段提供变量、条件、循环和宏,用来生成 SQL。 dbt 的宏(Macro)系统让你能把重复的 SQL 逻辑封装成可复用的函数,践行 DRY 原则。


为什么需要 Jinja

假设你有 15 个不同数据源,每个源都需要做同样的清洗逻辑:

-- stg_shopify__orders.sql
SELECT
    id AS order_id,
    LOWER(TRIM(status)) AS status,
    ROUND(amount * 100)::BIGINT AS amount_cents,
    ...

-- stg_amazon__orders.sql
SELECT
    order_id,
    LOWER(TRIM(status)) AS status,
    ROUND(amount * 100)::BIGINT AS amount_cents,
    ...

-- stg_wechat__orders.sql
-- 又是一样的逻辑...

每次修改逻辑,需要改 15 个文件。这就是 Jinja 宏要解决的问题:把重复的 SQL 逻辑封装成一个宏,一处定义,处处引用。


Jinja 基础语法

dbt 使用 Jinja2 模板引擎,有三种基本语法:

{{ expression }}    # 输出表达式的值(变量、函数调用等)
{% statement %}     # 执行语句(if/for/set 等,不输出内容)
{# comment #}       # 注释(不输出到最终 SQL)

变量

-- 在 SQL 文件中使用 Jinja 变量
{% set start_date = '2024-01-01' %}
{% set end_date = '2024-12-31' %}

SELECT *
FROM {{ ref('fct_orders') }}
WHERE order_date BETWEEN '{{ start_date }}' AND '{{ end_date }}'

编译后的 SQL:

SELECT *
FROM analytics.fct_orders
WHERE order_date BETWEEN '2024-01-01' AND '2024-12-31'

条件语句

-- 根据 target(dev/prod)改变行为
SELECT
    order_id,
    customer_id,
    total_price_cents
FROM {{ ref('stg_orders') }}

{% if target.name == 'dev' %}
    -- 开发环境只取最近 30 天数据,加快查询速度
    WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
{% elif target.name == 'prod' %}
    -- 生产环境取 2020-01-01 起的数据;若需全量,应按业务范围调整或移除此过滤条件
    WHERE created_at >= '2020-01-01'
{% endif %}

tip target 变量

target.name 返回当前使用的 target 名称(在 profiles.yml 中定义)。 其他有用的 target 属性:target.schema、target.database、target.type(数据库类型)

循环

-- 生成多个 UNION ALL(不同状态的聚合)
{% set statuses = ['pending', 'paid', 'refunded', 'cancelled'] %}

{% for status in statuses %}
SELECT
    '{{ status }}'  AS status,
    COUNT(*)        AS order_count,
    SUM(total_price_cents) AS total_cents
FROM {{ ref('fct_orders') }}
WHERE financial_status = '{{ status }}'

{% if not loop.last %}
UNION ALL
{% endif %}
{% endfor %}

自定义宏(Macro)

宏是 dbt 中的"函数"——接收参数,返回 SQL 片段。

创建宏

-- macros/cents_to_dollars.sql
-- 宏:把分转换为元(带格式化)

{% macro cents_to_dollars(column_name, decimal_places=2) %}
    ROUND({{ column_name }} / 100.0, {{ decimal_places }})
{% endmacro %}

调用宏

-- models/marts/core/fct_orders.sql

SELECT
    order_id,
    customer_id,
    total_price_cents,
    -- 调用宏,生成可读的金额字段
    {{ cents_to_dollars('total_price_cents') }}    AS total_price,
    {{ cents_to_dollars('total_price_cents', 0) }} AS total_price_rounded

FROM {{ ref('stg_orders') }}

编译后的 SQL:

SELECT
    order_id,
    customer_id,
    total_price_cents,
    ROUND(total_price_cents / 100.0, 2)  AS total_price,
    ROUND(total_price_cents / 100.0, 0)  AS total_price_rounded
FROM analytics.stg_orders

实用宏示例:动态日期截断

-- macros/date_spine.sql
-- 宏:生成一系列时间维度字段

{% macro date_dimensions(date_column) %}
    {{ date_column }}                                  AS raw_date,
    DATE_TRUNC('day', {{ date_column }})               AS date_day,
    DATE_TRUNC('week', {{ date_column }})              AS date_week,
    DATE_TRUNC('month', {{ date_column }})             AS date_month,
    DATE_TRUNC('quarter', {{ date_column }})           AS date_quarter,
    DATE_TRUNC('year', {{ date_column }})              AS date_year,
    EXTRACT(DOW FROM {{ date_column }})                AS day_of_week,
    EXTRACT(DAY FROM {{ date_column }})                AS day_of_month,
    TO_CHAR({{ date_column }}, 'YYYY-MM')              AS year_month
{% endmacro %}

使用:

-- models/marts/fct_orders.sql

SELECT
    order_id,
    {{ date_dimensions('created_at') }},
    total_price_cents
FROM {{ ref('stg_orders') }}

实用宏示例:安全除法与代理键

以下宏展示安全除法和多字段拼接哈希。它们不提供日期函数适配;函数支持、类型转换及 NULL 处理需按目标数据库确认。

-- macros/safe_divide.sql
-- 安全除法:分母为零时返回 NULL;表达式类型与数据库适用性需按目标环境确认

{% macro safe_divide(numerator, denominator) %}
    CASE
        WHEN ({{ denominator }}) = 0 THEN NULL
        ELSE ({{ numerator }}) * 1.0 / ({{ denominator }})
    END
{% endmacro %}
-- macros/generate_surrogate_key.sql
-- 生成代理键(将多个字段拼接哈希)

{% macro generate_surrogate_key(field_list) %}
    MD5(CONCAT_WS('|',
        {% for field in field_list %}
            CAST({{ field }} AS TEXT)
            {% if not loop.last %}, {% endif %}
        {% endfor %}
    ))
{% endmacro %}

使用:

SELECT
    {{ generate_surrogate_key(['order_id', 'item_id']) }} AS surrogate_key,
    order_id,
    item_id,
    quantity,
    {{ safe_divide('amount_cents', 'quantity') }} AS unit_price_cents
FROM {{ ref('stg_order_items') }}

常用内置宏

dbt 提供了一些非常有用的内置宏:

var():读取项目变量

# dbt_project.yml
vars:
  start_date: '2024-01-01'
  payment_methods: ['credit_card', 'alipay', 'wechat_pay']
  is_test_run: false
-- models/fct_orders.sql

{% set payment_methods = var('payment_methods') %}

SELECT
    order_id,
    {% for method in payment_methods %}
    SUM(CASE WHEN payment_method = '{{ method }}' THEN amount_cents END)
        AS {{ method }}_amount_cents
    {% if not loop.last %},{% endif %}
    {% endfor %}
FROM {{ ref('stg_orders') }}
WHERE created_at >= '{{ var("start_date") }}'
GROUP BY order_id

{% if var('is_test_run', default=false) %}
LIMIT 100
{% endif %}

命令行覆盖变量:

# 临时覆盖变量值
dbt run --vars '{"start_date": "2024-06-01", "is_test_run": true}'

env_var():读取环境变量

-- 在 SQL 中读取环境变量
SELECT *
FROM {{ ref('stg_orders') }}
WHERE shop_id = '{{ env_var("SHOP_ID") }}'

run_query():在宏中执行 SQL 查询

-- macros/get_column_values.sql
-- 宏:动态获取某列的所有唯一值

{% macro get_column_values(table, column) %}
    {% set query %}
        SELECT DISTINCT {{ column }}
        FROM {{ table }}
        ORDER BY 1
    {% endset %}

    {% set results = run_query(query) %}
    {% if execute %}
        {% set values = results.columns[0].values() %}
        {{ return(values) }}
    {% endif %}
{% endmacro %}

包管理(packages.yml)

dbt 包是可复用的宏和模型集合,类似 Python 的 pip 包:

安装包

# packages.yml(项目根目录)

packages:
  # dbt 官方出品的实用工具包(必装)
  - package: dbt-labs/dbt_utils
    version: [">=1.0.0", "<2.0.0"]

  # 丰富的数据质量测试(强烈推荐)
  - package: calogica/dbt_expectations
    version: [">=0.10.0", "<0.11.0"]

  # 日期维度表生成
  - package: dbt-labs/dbt_date
    version: [">=0.10.0", "<0.11.0"]

  # 数据审计比较(用于比较关系或查询结果,不会自动添加元数据列)
  - package: dbt-labs/audit_helper
    version: [">=0.9.0", "<0.10.0"]
# 安装包
dbt deps

dbt-utils 核心功能

dbt-utils 是最常用的 dbt 扩展包:

-- 1. generate_surrogate_key:生成代理键
SELECT
    {{ dbt_utils.generate_surrogate_key(['order_id', 'item_id']) }} AS sk,
    order_id,
    item_id
FROM {{ ref('stg_order_items') }}


-- 2. star:SELECT 全部列并排除某些列(解决 SELECT * 的问题)
SELECT
    {{ dbt_utils.star(from=ref('stg_orders'), except=["internal_notes", "_fivetran_synced"]) }}
FROM {{ ref('stg_orders') }}


-- 3. pivot:生成行转列的聚合表达式;此例汇总整张支付表
SELECT
    {{ dbt_utils.pivot(
        column='payment_method',
        values=dbt_utils.get_column_values(ref('stg_payments'), 'payment_method'),
        agg='SUM',
        then_value='amount_cents'
    ) }}
FROM {{ ref('stg_payments') }}


-- 4. date_spine:生成日期序列(用于日期维度表)
{{ dbt_utils.date_spine(
    datepart="day",
    start_date="cast('2020-01-01' as date)",
    end_date="cast('2025-12-31' as date)"
) }}
# 在测试中使用 dbt-utils

models:
  - name: fct_orders
    tests:
      # 组合唯一性(多字段联合唯一)
      - dbt_utils.unique_combination_of_columns:
          combination_of_columns:
            - order_id
            - item_id

      # 数据新鲜度测试(检查 created_at 的最新值是否在近 24 小时内)
      - dbt_utils.recency:
          datepart: hour
          field: created_at
          interval: 24

      # 表间相同列比较
      - dbt_utils.equality:
          compare_model: ref('fct_orders_v2')
          compare_columns:
            - order_id
            - total_price_cents

DRY 原则在 dbt 中的实践

DRY(Don't Repeat Yourself)是软件工程的基本原则:同一段逻辑只写一次。

反例:重复的业务逻辑

-- fct_orders.sql
CASE
    WHEN order_count = 0 THEN 'no_orders'
    WHEN order_count = 1 THEN 'new'
    WHEN order_count BETWEEN 2 AND 5 THEN 'repeat'
    ELSE 'loyal'
END AS customer_segment

-- fct_customer_monthly.sql
CASE
    WHEN order_count = 0 THEN 'no_orders'
    WHEN order_count = 1 THEN 'new'
    WHEN order_count BETWEEN 2 AND 5 THEN 'repeat'
    ELSE 'loyal'
END AS customer_segment

-- 如果分层逻辑变了,需要改两处(很容易遗漏)

正例:用宏封装

-- macros/customer_segment.sql

{% macro get_customer_segment(order_count_column) %}
    CASE
        WHEN {{ order_count_column }} = 0 THEN 'no_orders'
        WHEN {{ order_count_column }} = 1 THEN 'new'
        WHEN {{ order_count_column }} BETWEEN 2 AND 5 THEN 'repeat'
        ELSE 'loyal'
    END
{% endmacro %}

使用:

-- fct_orders.sql
SELECT
    order_id,
    {{ get_customer_segment('lifetime_order_count') }} AS customer_segment

-- fct_customer_monthly.sql
SELECT
    customer_id,
    {{ get_customer_segment('monthly_order_count') }} AS customer_segment

修改逻辑时只改宏定义一处,所有引用自动更新。


调试 Jinja

在 dbt 中调试 Jinja 的技巧:

# 查看 Jinja 编译后的 SQL
dbt compile --select my_model
cat target/compiled/my_project/models/my_model.sql

# 在宏中打印调试信息
{% do log("Debug: variable = " ~ my_variable, info=True) %}

总结

概念 作用 示例
{{ }} 输出表达式 {{ ref('stg_orders') }}
{% %} 执行逻辑 {% if target.name == 'dev' %}
宏(Macro) 封装可复用的 SQL 逻辑 {{ cents_to_dollars('amount') }}
var() 读取项目变量 {{ var('start_date') }}
dbt-utils 常用工具包 {{ dbt_utils.generate_surrogate_key([...]) }}
packages.yml 管理外部依赖 dbt deps 安装

下一步

09-Seeds与Snapshots - 用 seed 管理配置表,用 snapshot 实现历史数据追踪(SCD Type 2)


最后更新:2026-03-18

#dbt #Jinja #宏 #代码复用 #dbt-utils #DRY原则


info 前置知识

  • 掌握基础 dbt Model 的写法,参考 dbt Model 基础
  • 了解 Jinja2 模板语言的基本语法({{ }}、{% %})
  • 熟悉 Python 的函数定义概念(宏本质上是可复用的函数)

掌握检查

  • [ ] 能在 dbt Model 中使用 {% if target.name == 'dev' %} 条件语句,实现开发环境只处理近 7 天数据的效果
  • [ ] 能写一个简单的宏函数(如"金额分转元的除法"),并在多个模型中通过 {{ cents_to_dollars('amount') }} 调用
  • [ ] 知道 dbt compile --select 模型名 的作用,以及在哪个目录找到编译后的 SQL 来调试 Jinja 问题
  • [ ] 能用 dbt-utils 的 generate_surrogate_key() 生成代理键,以及用 star() 实现排除特定列的 SELECT
  • [ ] 理解 var() 和 env_var() 的区别:前者从 dbt_project.yml 或命令行读取,后者读取操作系统环境变量

实践练习

基础题:你的项目中有 3 个模型都需要把金额从"分"转成"元"(除以 100)。把这个逻辑写成一个宏 cents_to_dollars(column_name),放在 macros/ 目录,然后在这 3 个模型中替换掉重复的除法表达式。运行 dbt compile 验证宏展开后的 SQL 正确。

进阶题:用 Jinja 实现一个"环境感知"的数据过滤宏:在 dev 环境只处理过去 30 天的数据(加速开发调试),在 prod 环境处理全量数据。在 fct_orders 模型中使用这个宏,运行 dbt run --target dev 和 --target prod 对比执行结果。