Salta al contenuto principale

03 - Jinja e Macro in dbt: SQL Dinamico, Templating e Macro Personalizzate

dbt 15 min Jinja e Macro in dbt: SQL Dinamico, Templating e Macro Personalizzate Jinja trasforma dbt da strumento SQL a framework di trasformazione...

Cosa ne pensi di questo articolo?

In Breve

dbt 15 min Jinja e Macro in dbt: SQL Dinamico, Templating e Macro Personalizzate Jinja trasforma dbt da strumento SQL a framework di trasformazione...

dbt 15 min

Jinja e Macro in dbt: SQL Dinamico, Templating e Macro Personalizzate

Jinja trasforma dbt da strumento SQL a framework di trasformazione programmabile: variabili, loop, condizioni e macro riutilizzabili per DRY SQL. Esplora dbt-utils (la libreria standard) e come scrivere macro personalizzate con parametri tipati.

dbt jinja dbt macro dbt variables dbt-utils dynamic SQL

Jinja2 in dbt: Cos'è e Perché Esiste

dbt usa Jinja2, il motore di templating Python, per aggiungere programmabilità all'SQL. Tutto ciò che sta tra doppie parentesi graffe {{ }} è un'espressione Jinja, tutto ciò che sta tra {% %} è un'istruzione di controllo (if, for, set).

Prima di eseguire ogni modello nel warehouse, dbt compila il template Jinja in SQL puro. Puoi vedere il SQL compilato nella cartella target/compiled/ dopo ogni dbt run.

Variabili: var() e env_var()

dbt mette a disposizione due funzioni per accedere a variabili nel codice SQL:

var(): Variabili del Progetto

-- 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(): Variabili d'Ambiente

-- 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') }}"

Condizioni if/else nei Modelli

Le istruzioni condizionali Jinja permettono di scrivere modelli che si comportano diversamente in base al contesto (ambiente, variabili, tipo di materializzazione):

-- 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(): Pattern Fondamentale

La macro built-in is_incremental() è usata nei modelli incrementali per aggiungere il filtro temporale solo quando il modello viene eseguito in modalità incrementale (non in full refresh):

-- 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

Loop for: Generazione Dinamica di SQL

I loop Jinja sono potentissimi per generare SQL ripetitivo senza copia-incolla:

-- 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: Funzioni SQL Riutilizzabili

Le macro sono il meccanismo di riutilizzo del codice in dbt: funzioni Jinja che accettano parametri e restituiscono SQL. Vanno nella directory macros/.

Macro Semplice: Pulizia dei Valori Nulli

-- 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') }}

Macro Avanzata: Generazione di UNION ALL Dinamica

-- 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 con run_query(): Interrogare il Warehouse nelle Macro

-- 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: La Libreria Standard

dbt-utils è il package più utilizzato nell'ecosistema dbt. Fornisce macro comuni che ogni progetto probabilmente reinventerebbe da zero:

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

# Installa con:
# dbt deps

Le Macro dbt-utils più Usate

-- 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'
) }}

Best Practices per le Macro

Linee Guida per Macro di Qualità

  • Usa if execute per le macro che eseguono query: il DAG viene parsato più volte e non tutte le fasi necessitano dell'esecuzione reale
  • Documenta le macro nello stesso modo dei modelli - dbt genererà documentazione anche per le macro con docstrings Jinja
  • Preferisci package consolidati (dbt-utils, dbt-expectations) alla reinvenzione della ruota - sono testati da migliaia di progetti
  • Mantieni le macro semplici: se una macro è difficile da leggere, probabilmente è meglio dividerla o esprimere la logica come SQL esplicito nel modello

Anti-pattern: Macro Over-engineered

L'errore più comune è usare macro per tutto. Le macro aggiungono livelli di indirezione che rendono il codice meno leggibile. Usale per logica genuinamente riutilizzabile (3+ utilizzi nel progetto). Per SQL usato una o due volte, il codice esplicito è più manutenibile.

Conclusioni e Prossimi Passi

Con Jinja e le macro, dbt smette di essere un semplice runner di SQL e diventa un framework di trasformazione programmabile. Le variabili rendono i modelli adattabili agli ambienti, i loop eliminano la ripetizione, le macro incapsulano logica riutilizzabile.

Il prossimo articolo affronta un tema cruciale per le performance in produzione: le materializzazioni. Quando usare view, table, modelli incrementali e snapshot - e come scegliere la strategia giusta per ciascun dataset.

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti