Course Builder个人学习工作台 · 本地模式
dbt 入门:把 SQL 变成可信的数据流水线

Jinja 与宏:复用一条金额规则

目标:将分到元的转换抽成宏,并用编译结果确认宏生成的 SQL。

stg_orders 和 customer_summary 都有从分到元的换算。两处时复制还不难,但规则一旦重复很多次,改精度时容易漏掉一处。宏可以为一段重复 SQL 提供一个名字。不要因为会写宏就把所有 SQL 都抽象起来:本课抽取的是有稳定含义的小表达式。

Jinja 是生成文本的模板语言。{{ 表达式 }} 输出模板结果;{% ... %} 控制模板结构。dbt 先处理这些模板,再把生成的 SQL 交给数据库。我们的金额宏不是在数据库中注册一个逐行运行的函数,它生成的是一个 SQL 表达式。

创建并调用一个宏

创建 macros/cents_to_currency.sql:

{% macro cents_to_currency(expression) -%}
cast(({{ expression }}) / 100.0 as decimal(12,2))
{%- endmacro %}

macro 定义名字和参数,endmacro 结束定义;分隔符旁的 - 只收紧模板空白。参数 expression 是一段 SQL 文本。加括号是为了在传入加法等表达式时仍按预期先计算它,再除以 100。

将 models/staging/stg_orders.sql 完整替换为:

select
    cast(order_id as integer) as order_id,
    cast(customer_id as integer) as customer_id,
    cast(order_date as date) as order_date,
    lower(trim(status)) as status,
    cast(amount_cents as integer) as amount_cents,
    {{ cents_to_currency('amount_cents') }} as amount,
    cast(updated_at as timestamp) as updated_at
from {{ source('shop', 'orders') }}

再对第 6 课的 models/marts/customer_summary.sql 做一处替换,其他行保留:

原行:
    cast(coalesce(t.revenue_cents, 0) / 100.0 as decimal(12,2)) as revenue
替换为:
    {{ cents_to_currency('coalesce(t.revenue_cents, 0)') }} as revenue

这里的引号让 Jinja 接收字符串 'amount_cents',然后把它作为 SQL 列表达式输出;输出不是 SQL 字符串常量 'amount_cents'。如果去掉模板参数的引号,Jinja 会试着寻找同名模板变量,和引用数据库列是两回事。

先预测结果值是否变化,再运行:

uv run dbt compile --profiles-dir . --select stg_orders customer_summary
cat target/compiled/shop_dbt/models/staging/stg_orders.sql
uv run dbt build --profiles-dir . --exclude resource_type:seed
uv run dbt show --profiles-dir . --inline "select customer_id, revenue from {{ ref('customer_summary') }} order by customer_id"

预期编译 SQL 包含 cast((amount_cents) / 100.0 as decimal(12,2)),不存在未展开的 cents_to_currency 调用。汇总金额仍是 12、40、0、0,15 项测试仍通过。这样的重构应该保持业务含义不变。

用一个常量检查表达式括号

引导练习:先手算 (100 + 50) / 100,再执行:

uv run dbt show --profiles-dir . --inline "select {{ cents_to_currency('100 + 50') }} as amount"

应得到 1.50。若宏不加括号,表达式可能变成 100 + 50 / 100,结果含义就变了。抽象一段 SQL 时,运算优先级也是接口的一部分。

独立练习:用宏展示 999 分,不改任何输入文件。预测结果,并解释为什么不能把它作为真实货币汇率换算器。

参考答案
uv run dbt show --profiles-dir . --inline "select {{ cents_to_currency('999') }} as amount"

应为 9.99。这里固定每 100 个最小单位等于 1 个展示单位,且保留两位小数;它没有币种、汇率、换汇日期等信息。我们仅把本课的金额表达式集中管理。

若提示宏未定义,检查文件是否位于 macros 目录、宏名是否拼对;若出现奇怪的数据库列错误,先看 compiled SQL,确认模板最终生成了什么。宏越复杂,编译后的 SQL 越值得阅读。不要在 SQL 的每个小表达式外再套一个缺少业务含义的宏。

本课保留金额宏及两个调用它的模型。没有引入第三方包,也不需要执行 dbt deps。

参考资料