DHDataHọc
Lý thuyếtBài 6/85 phút đọc+10 XP

Jinja và macro

Bạn sẽ học được

  • Phân biệt {{ ... }} và {% ... %} trong Jinja.
  • Viết macro có tham số và dùng lại ở nhiều model.
  • Truyền biến bằng --vars thay vì sửa code.
  • Đọc biến môi trường và tên target trong model.
  • Biết khi nào không nên viết macro.

Khái niệm cốt lõi

dbt biên dịch mỗi model qua Jinja trước khi gửi SQL xuống kho. Jinja có hai loại thẻ:

Cú phápVai tròVí dụ
{{ ... }}In ra một giá trị{{ ref('stg_orders') }}
{% ... %}Điều khiển luồng, không in gì{% if is_incremental() %}

Chỉ hai loại này là đủ để làm hầu hết việc: ref, source, config, var đều là hàm có sẵn, và mọi logic điều kiện nằm trong {% %}.

Điểm quan trọng: Jinja chạy lúc biên dịch, không chạy trong kho. {{ 1 + 1 }} cho ra chữ 2 trong câu SQL; bản thân phép cộng không được gửi xuống database. Vì vậy không thể dùng Jinja để tính toán trên dữ liệu.

Jinja tạo ra SQL. SQL mới là thứ tính toán trên dữ liệu.

Ví dụ chạy được

Biến và điều kiện

code
-- models/marts/fct_orders.sql
{{ config(materialized='table', tags=['daily']) }}

select
    order_date,
    segment,
    count(*)        as orders,
    sum(net_amount) as revenue
from {{ ref('int_orders_enriched') }}
where order_date >= '{{ var("start_date", "2026-01-01") }}'
  and order_date <  '{{ var("end_date", "2027-01-01") }}'
group by 1, 2

Tham số thứ hai của var là giá trị mặc định. Chạy với khoảng thời gian khác mà không sửa file:

code
dbt run --select fct_orders --vars '{"start_date": "2026-03-01", "end_date": "2026-04-01"}'

Macro

Macro là hàm Jinja đặt trong macros/, dùng lại ở bất kỳ model nào:

code
-- macros/cents_to_dollars.sql
{% macro cents_to_dollars(column_name, scale=2) -%}
    round({{ column_name }} / 100.0, {{ scale }})
{%- endmacro %}

Dùng trong model:

code
select
    order_id,
    {{ cents_to_dollars('amount_cents') }}     as amount,
    {{ cents_to_dollars('discount_cents') }}   as discount,
    {{ cents_to_dollars('tax_cents', 4) }}     as tax
from {{ ref('stg_orders') }}

Sau biên dịch, mỗi lời gọi được thay bằng biểu thức SQL tương ứng. Sửa cách đổi đơn vị tiền chỉ cần sửa một file, mọi model dùng macro đều đổi theo.

Vòng lặp

Một trường hợp hay gặp: union nhiều bảng giống nhau theo tháng.

code
{% set months = ['2026-01', '2026-02', '2026-03'] %}

with raw as (
    {% for m in months %}
    select '{{ m }}' as thang, * from {{ source('raw', 'events_' ~ m | replace('-', '_')) }}
    {% if not loop.last %}union all{% endif %}
    {% endfor %}
)
select thang, count(*) as so_su_kien
from raw
group by 1

Vòng lặp tạo ra ba khối select ... union all, điều kiện loop.last bỏ union all ở khối cuối.

Lặp để sinh SQL cho các bảng có cấu trúc giống nhau là hợp lý. Lặp để sinh ra 50 model gần như trùng nhau là dấu hiệu nên dùng union all với cột nguồn, không phải Jinja.

Biến môi trường và target

code
select *
from {{ ref('stg_orders') }}
{% if target.name != 'prod' %}
  -- trên dev chỉ lấy 1000 dòng để chạy nhanh
  limit 1000
{% endif %}

target.name cho biết đang chạy target nào. Đây là cách phổ biến để dev chạy nhanh mà production vẫn đầy đủ.

env_var đọc biến môi trường — thường dùng trong profiles.yml, ít khi trong model:

code
{{ env_var('DBT_SCHEMA') }}

Gói macro dựng sẵn

Không cần viết lại mọi thứ. Khai báo packages.yml:

code
packages:
  - package: dbt-labs/dbt_utils
    version: [">=1.0.0", "<2.0.0"]

Chạy dbt deps, sau đó dùng được:

MacroCông dụng
dbt_utils.generate_surrogate_key([...])Tạo khoá thay thế từ nhiều cột
dbt_utils.star(ref('model'), except=[...])Chọn mọi cột trừ vài cột
dbt_utils.date_spine(...)Sinh dải ngày liên tục
dbt_utils.union_relations(...)Union các model cùng cấu trúc

generate_surrogate_key đặc biệt hữu ích khi bảng không có khoá đơn:

code
select
    {{ dbt_utils.generate_surrogate_key(['order_id', 'product_id']) }} as order_item_key,
    order_id,
    product_id,
    quantity
from {{ ref('stg_order_items') }}

Kết quả là một chuỗi băm ổn định — cùng cặp order_id, product_id luôn cho cùng khoá, dùng được cho unique_key của incremental.

Khi nào không nên viết macro

Macro che giấu SQL. Người đọc model chỉ thấy {{ tinh_doanh_thu(...) }} và phải mở file macro mới hiểu. Vì vậy:

  • Viết macro khi cùng một biểu thức lặp ở ba model trở lên.
  • Không viết macro cho logic chỉ dùng một lần.
  • Không viết macro quá 20 dòng — tách nhỏ hoặc viết SQL trực tiếp.
  • Đặt tên macro mô tả kết quả, không mô tả cách làm: net_amount tốt hơn subtract_discount_from_amount.

Thực hành

  1. Viết macro cents_to_dollars và dùng ở hai model.
  2. Thêm var('start_date') cho một mart, chạy với hai giá trị khác nhau.
  3. Dùng target.name để giới hạn 1.000 dòng trên dev.
  4. Cài dbt_utils và dùng generate_surrogate_key cho một bảng chi tiết.
  5. In SQL sau biên dịch bằng dbt compile --select fct_orders, rồi mở file trong target/compiled/.

Lỗi thường gặp

Triệu chứngNguyên nhânCách sửa
Compilation Error: unexpected end of templateThiếu {% endif %}Đếm cặp if/endif, for/endfor
var not foundKhông có giá trị mặc định và không truyền --varsThêm tham số mặc định
Macro không được tìm thấySai tên file hoặc sai thư mụcĐặt trong macros/, chạy dbt parse
SQL sau biên dịch rỗngDùng {# ... #} nhầm chỗ hoặc {% %} in giá trịDùng {{ }} khi cần in
Test chạy 20 phút trên devDev chạy toàn bộ dữ liệuThêm limit theo target.name
Hai model cùng macro ra kết quả khácMacro nhận tham số khác nhauKiểm tra target/compiled

Tóm tắt

  • {{ }} in giá trị, {% %} điều khiển luồng; cả hai chạy lúc biên dịch.
  • var() cho tham số, target.name cho môi trường, env_var() cho bí mật.
  • Macro dùng lại logic lặp ở từ ba model trở lên.
  • dbt_utils có sẵn generate_surrogate_key, star, date_spine.
  • Luôn đọc file trong target/compiled/ khi nghi ngờ Jinja sinh sai.

Dùng ← → để chuyển bài