dbt Jinja 與 Macros:提升程式碼重用性

什麼是 Jinja?

你可能已經注意到,dbt 的 SQL 檔案中經常出現 {{ ... }} 這樣的符號。這其實是 Jinja,一個 Python 的模板語言。

dbt 使用 Jinja 來讓 SQL 變得動態化。SQL 本身是靜態的,但加上 Jinja 後,我們可以:

  • 使用變數 (Variables)
  • 使用控制結構 (if/else, for loops)
  • 引用環境變數 (Environment Variables)

基本語法

  • {{ ... }}: 表達式 (Expressions),會將結果輸出到 SQL 中。例如 {{ ref('users') }}
  • {% ... %}: 語句 (Statements),用於控制邏輯,不會直接輸出。例如 {% if ... %}
  • {# ... #}: 註解 (Comments),不會出現在編譯後的 SQL 中。

範例:使用迴圈

假設我們要對這三個付款方式欄位做加總,不想手寫三次:

{% set payment_methods = ["bank_transfer", "credit_card", "gift_card"] %}

select
    order_id,
    {% for payment_method in payment_methods %}
    sum(case when payment_method = '{{ payment_method }}' then amount else 0 end) as {{ payment_method }}_amount,
    {% endfor %}
    sum(amount) as total_amount
from {{ ref('raw_payments') }}
group by 1

編譯後的 SQL 會自動展開成三行 sum case when,非常方便!

進階 Jinja 技巧

除了基本的變數與迴圈,還有幾個在專案中非常常用的技巧:

使用變數 (Variables)

你可以定義全域變數,讓多個 model 共用同一個值 (例如:開始日期、稅率)。

設定變數 (dbt_project.yml)

vars:
  start_date: '2023-01-01'

使用變數 (model.sql)

select *
from {{ ref('orders') }}
where created_at >= '{{ var("start_date") }}'

此外,你也可以在 CLI 執行時動態覆蓋變數,這在修復資料 (Backfill) 時非常有用:

dbt run --vars '{"start_date": "2022-01-01"}'

資料安全性:環境變數 (Environment Variables)

永遠不要將密碼或敏感 Token 寫死在程式碼中。dbt 提供了 env_var() 函數來讀取系統環境變數。

-- profiles.yml 範例
pass: "{{ env_var('DBT_PASSWORD') }}"

空白字元控制 (Whitespace Control)

Jinja 預設會保留程式碼中的換行與空白。如果你在意編譯出來的 SQL 是否美觀,可以使用減號 - 來去除空白。

  • {%- ... %}:去除左邊的空白/換行
  • {% ... -%}:去除右邊的空白/換行

例如:

{%- for col in ['id', 'name'] -%}
  {{ col }},
{%- endfor -%}

編譯結果會變成緊湊的 id,name,,而不會有一堆空行。

什麼是 Macros?

Macros 就像是程式語言中的 Function (函式)。它允許我們將常用的 SQL 片段封裝起來,在不同的 models 中重複使用。

定義 Macro

Macros 必須定義在 macros/ 目錄下的 .sql 檔案中。

範例:建立一個 macros/cents_to_dollars.sql,用來將美分轉成美元:

{% macro cents_to_dollars(column_name, scale=2) %}
    ({{ column_name }} / 100)::numeric(16, {{ scale }})
{% endmacro %}

使用 Macro

在任何 model 中,就可以像呼叫函數一樣使用它:

select
    id,
    {{ cents_to_dollars('amount_cents') }} as amount_usd,
    {{ cents_to_dollars('tax_cents', scale=4) }} as tax_usd
from {{ ref('payments') }}

編譯後的 SQL:

select
    id,
    (amount_cents / 100)::numeric(16, 2) as amount_usd,
    (tax_cents / 100)::numeric(16, 4) as tax_usd
from ...

常用的內建 Macros

dbt 已經內建了許多好用的 macros,不需要自己重寫:

  • {{ return(foo) }}: 回傳值。
  • {{ log("message", info=True) }}: 在 CLI 輸出 log 訊息 (debug 用)。
  • {{ run_query(sql) }}: 直接執行一段 SQL 並取得結果 (進階用法)。

總結

Jinja 與 Macros 是區分「會寫 SQL」與「精通 dbt」的分水嶺。善用它們可以大幅減少重複程式碼 (DRY),讓你的專案更整潔、更好維護。