Jinja2 in dbt: wat het is en waarom het bestaat

dbt VS Jinja2, de Python-sjabloonengine, om programmeerbaarheid toe te voegen naar de SQL. Alles wat tussen dubbele accolades past {{ }} is een Jinja-uitdrukking, alles daartussenin {% %} het is een controle-instructie (if, for, set).

Voordat elk model in het magazijn wordt uitgevoerd, moet dbt compileren de Jinja-sjabloon in pure SQL. U kunt de gecompileerde SQL in de map zien target/compiled/ na elk dbt run.

Variabelen: var() en env_var()

dbt biedt twee functies voor toegang tot variabelen in SQL-code:

var(): Projectvariabelen

-- In dbt_project.yml puoi definire variabili globali:
# dbt_project.yml
vars:
  start_date: '2024-01-01'
  lookback_days: 30
  payment_methods: ['credit_card', 'paypal', 'bank_transfer']

-- Usale nei modelli con var():
SELECT *
FROM {{ ref('stg_orders') }}
WHERE created_at >= '{{ var("start_date") }}'::date

-- Puoi sovrascrivere una variabile da CLI:
-- dbt run --vars '{"start_date": "2025-01-01", "lookback_days": 7}'

env_var(): Omgevingsvariabelen

-- Accedi alle variabili d'ambiente del sistema
SELECT *
FROM {{ source('raw', 'events') }}
WHERE environment = '{{ env_var("DBT_ENVIRONMENT", "development") }}'
-- Il secondo parametro è il valore di default (opzionale)

-- Nei profiles.yml per le credenziali (prattica consigliata):
# profiles.yml
my_profile:
  outputs:
    prod:
      type: snowflake
      account: "{{ env_var('SNOWFLAKE_ACCOUNT') }}"
      password: "{{ env_var('SNOWFLAKE_PASSWORD') }}"

if/else-voorwaarden in sjablonen

Met voorwaardelijke instructies van Jinja kunt u modellen schrijven die zich anders gedragen op basis van context (omgeving, variabelen, type materialisatie):

-- models/marts/finance/orders_with_taxes.sql
-- Logica di calcolo tasse diversa per paese

SELECT
    order_id,
    customer_id,
    total_amount,

    {% if var("target_market") == "US" %}
    total_amount * 0.08 AS tax_amount,     -- aliquota USA semplificata
    {% elif var("target_market") == "IT" %}
    total_amount * 0.22 AS tax_amount,     -- IVA italiana
    {% else %}
    total_amount * 0.20 AS tax_amount,     -- aliquota default EU
    {% endif %}

    total_amount + tax_amount AS total_with_tax

FROM {{ ref('stg_orders') }}
WHERE status = 'completed'

is_incremental(): Fundamenteel patroon

De ingebouwde macro is_incremental() het wordt gebruikt in incrementele modellen om toe te voegen het tijdelijke filter alleen wanneer het model in incrementele modus wordt uitgevoerd (niet volledig vernieuwd):

-- models/marts/events_daily.sql
{{ config(materialized='incremental', unique_key='event_date') }}

SELECT
    DATE_TRUNC('day', event_timestamp) AS event_date,
    event_type,
    COUNT(*) AS event_count,
    COUNT(DISTINCT user_id) AS unique_users
FROM {{ ref('stg_events') }}

-- Questo blocco viene incluso SOLO nelle esecuzioni incrementali
{% if is_incremental() %}
WHERE event_timestamp > (SELECT MAX(event_date) FROM {{ this }})
{% endif %}

GROUP BY 1, 2

Lus voor: dynamische SQL-generatie

Jinja-loops zijn zeer krachtig voor het genereren van repetitieve SQL zonder kopiëren en plakken:

-- Genera colonne per i giorni della settimana dinamicamente
SELECT
    customer_id,
    order_date,

    {% for day_num in range(1, 8) %}
    SUM(CASE WHEN DAYOFWEEK(order_date) = {{ day_num }}
             THEN total_amount
             ELSE 0 END) AS revenue_day_{{ day_num }}
    {% if not loop.last %},{% endif %}
    {% endfor %}

FROM {{ ref('stg_orders') }}
GROUP BY 1, 2
-- Pivot di metriche da una lista di variabile
{% set metrics = ['revenue', 'order_count', 'avg_order_value'] %}

SELECT
    month,
    region,
    {% for metric in metrics %}
    SUM(CASE WHEN metric_name = '{{ metric }}' THEN metric_value END) AS {{ metric }}
    {%- if not loop.last %},{% endif %}
    {% endfor %}
FROM {{ ref('metrics_unpivoted') }}
GROUP BY 1, 2

Macro's: herbruikbare SQL-functies

Macro's zijn het mechanisme voor hergebruik van code in dbt: Jinja-functies die parameters gebruiken en retourneer SQL. Ze gaan naar de directory macros/.

Eenvoudige macro: opschoning van nulwaarden

-- macros/utils/safe_divide.sql
-- Divisione sicura che evita division by zero

{% macro safe_divide(numerator, denominator, default_value=0) %}
    CASE
        WHEN {{ denominator }} = 0 OR {{ denominator }} IS NULL
        THEN {{ default_value }}
        ELSE {{ numerator }} / {{ denominator }}
    END
{% endmacro %}

-- Utilizzo nel modello:
SELECT
    customer_id,
    total_revenue,
    order_count,
    {{ safe_divide('total_revenue', 'order_count') }} AS avg_order_value
FROM {{ ref('customer_summary') }}

Geavanceerde macro: dynamische UNION ALL-generatie

-- macros/union_relations.sql
-- Crea UNION ALL da una lista di ref()

{% macro union_all_tables(relations) %}
    {% for relation in relations %}
        SELECT
            '{{ relation }}' AS source_table,
            *
        FROM {{ ref(relation) }}
        {% if not loop.last %}UNION ALL{% endif %}
    {% endfor %}
{% endmacro %}

-- Utilizzo:
-- {{ union_all_tables(['events_jan', 'events_feb', 'events_mar']) }}

Macro met run_query(): Het magazijn bevragen in macro's

-- macros/get_column_values.sql
-- Recupera valori distinti da una colonna per uso in loop

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

    {% set results = run_query(query) %}

    {% if execute %}           -- execute è False durante la fase di parsing
        {% set values = results.columns[0].values() %}
        {% do return(values) %}
    {% else %}
        {% do return([]) %}
    {% endif %}
{% endmacro %}

-- Utilizzo per un pivot dinamico:
{% set regions = get_column_values('stg_orders', 'region') %}

SELECT
    order_date,
    {% for region in regions %}
    SUM(CASE WHEN region = '{{ region }}' THEN revenue END) AS revenue_{{ region | lower | replace(' ', '_') }}
    {%- if not loop.last %},{% endif %}
    {% endfor %}
FROM {{ ref('orders_daily') }}
GROUP BY 1

dbt-utils: de standaardbibliotheek

dbt-utils het is het meest gebruikte pakket in het dbt-ecosysteem. Het biedt algemene macro's die elk project waarschijnlijk helemaal opnieuw zou uitvinden:

# packages.yml
packages:
  - package: dbt-labs/dbt_utils
    version: 1.3.0

# Installa con:
# dbt deps

De meest gebruikte dbt-utils-macro's

-- 1. generate_surrogate_key: chiave surrogata da più colonne (hash MD5)
SELECT
    {{ dbt_utils.generate_surrogate_key(['order_id', 'customer_id']) }} AS sk,
    order_id,
    customer_id
FROM {{ ref('stg_orders') }}

-- 2. unpivot: trasforma colonne in righe (simile a UNPIVOT SQL)
{{ dbt_utils.unpivot(
    relation=ref('orders_pivoted'),
    cast_to='float',
    exclude=['order_date', 'customer_id'],
    field_name='metric_name',
    value_name='metric_value'
) }}

-- 3. date_spine: genera una sequenza di date continua (per riempire i gap)
WITH date_spine AS (
    {{ dbt_utils.date_spine(
        datepart="day",
        start_date="cast('2024-01-01' as date)",
        end_date="current_date"
    ) }}
),
orders AS (
    SELECT DATE_TRUNC('day', created_at) AS order_date, SUM(amount) AS revenue
    FROM {{ ref('stg_orders') }}
    GROUP BY 1
)
-- LEFT JOIN per avere 0 anche nei giorni senza ordini
SELECT
    d.date_day,
    COALESCE(o.revenue, 0) AS revenue
FROM date_spine d
LEFT JOIN orders o ON d.date_day = o.order_date

-- 4. pivot: trasforma righe in colonne
{{ dbt_utils.pivot(
    column='status',
    values=['completed', 'pending', 'cancelled'],
    agg='count',
    then_value='order_id'
) }}

Beste praktijken voor macro's

Richtlijnen voor kwaliteitsmacro's

  • VS if execute voor macro's die query's uitvoeren: de DAG komt meerdere keren geparseerd en niet alle fasen vereisen daadwerkelijke uitvoering
  • Macro's documenteren op dezelfde manier als sjablonen: dbt zal genereren documentatie ook voor macro's met Jinja-docstrings
  • Geef de voorkeur aan geconsolideerde pakketten (dbt-utils, dbt-verwachtingen) naar het wiel opnieuw uitvinden – worden getest door duizenden projecten
  • Houd macro's eenvoudig: als een macro moeilijk te lezen is, het is waarschijnlijk beter om het te splitsen of de logica als expliciete SQL in de sjabloon uit te drukken

Anti-patroon: overontwikkelde macro's

De meest voorkomende fout is om voor alles macro's te gebruiken. Macro's voegen lagen toe indirectheid die de code minder leesbaar maakt. Gebruik ze voor echt herbruikbare logica (3+ toepassingen in het project). Voor SQL die één of twee keer wordt gebruikt, is de expliciete code dat wel beter onderhoudbaar.

Conclusies en volgende stappen

Met Jinja en macro's is dbt niet langer een eenvoudige SQL-runner, maar wordt het een raamwerk van programmeerbare transformatie. Variabelen maken modellen aanpasbaar aan omgevingen, d.w.z lussen elimineren herhaling, macro's kapselen herbruikbare logica in.

Het volgende artikel behandelt een cruciaal onderwerp voor prestatie in de productie: de materialisaties. Wanneer moet u weergaven, tabellen, incrementele modellen en momentopnamen gebruiken? en hoe u voor elke dataset de juiste strategie kiest.