Macros dbt : des bases Jinja aux patterns de production

Les macros donnent à dbt une grande partie de sa flexibilité, mais elles sont souvent mal utilisées. J’ai vu des projets avec des macros élégantes et réutilisables qui font gagner des heures de développement. J’ai aussi vu des projets où les macros créent plus de problèmes qu’elles n’en résolvent, dissimulant du SQL simple derrière des couches d’abstraction que personne ne peut déboguer.

Bien utiliser les macros, c’est avant tout savoir quand et comment les appliquer.

Ce guide vous accompagne des fondamentaux Jinja jusqu’aux patterns prêts pour la production. Vous découvrirez d’abord la mécanique, puis les packages essentiels qui résolvent les problèmes courants, et enfin les principes de conception qui gardent vos macros maintenables à mesure que votre projet grandit.

Pourquoi les macros comptent dans dbt

Tout projet dbt accumule du SQL répétitif. Les conversions de devises apparaissent dans une douzaine de modèles. La logique de troncature de dates est copiée d’un fichier à l’autre. La génération de clés de substitution suit le même pattern partout.

Les macros vous permettent d’écrire cette logique une seule fois et de la réutiliser. Ce sont des templates Jinja qui génèrent du SQL à la compilation. Au lieu de copier une conversion de devises de cinq lignes dans chaque modèle, vous appelez {{ cents_to_dollars('amount') }} et laissez dbt écrire le SQL à votre place.

Mais les macros ne sont pas toujours la solution. Du SQL simple et lisible l’emporte souvent sur une abstraction ingénieuse. Mieux vaut reconnaître quand l’abstraction apporte une vraie valeur plutôt que d’essayer de tout transformer en macro.

Les fondamentaux Jinja pour dbt

dbt utilise Jinja2 comme langage de templates, avec une particularité importante : les templates sont rendus deux fois. La première passe (phase de parsing) construit le DAG et détermine les dépendances. La seconde (phase d’exécution) génère le SQL effectif. Ce fonctionnement en deux phases surprend souvent les débutants.

Variables et structures de contrôle

Le tag {% set %} crée des variables. Combinez-le avec des boucles et des conditions pour générer du SQL dynamique :

{% set payment_methods = ["bank_transfer", "credit_card", "gift_card"] %}
{% for method in payment_methods %}
SUM(CASE WHEN payment_method = '{{ method }}' THEN amount END) AS order__{{ method }}_amount
{{ ',' if not loop.last }}
{% endfor %}

Ce code génère trois colonnes SUM(CASE WHEN...) sans écrire chacune manuellement. La variable loop.last gère les virgules en fin de ligne, un problème classique du SQL généré.

Les espaces blancs comptent pour la lisibilité du SQL compilé. Utilisez {%- pour supprimer les espaces avant et -%} pour les supprimer après. Le SQL compilé sera bien plus facile à déboguer.

Anatomie d’une macro

Les macros se trouvent dans des fichiers .sql sous le répertoire macros/. Voici une macro de transformation simple :

{% macro cents_to_dollars(column_name, scale=2) %}
ROUND({{ column_name }} / 100, {{ scale }})
{% endmacro %}

Appelez-la avec {{ cents_to_dollars('amount_cents') }} ou modifiez la précision par défaut avec {{ cents_to_dollars('amount_cents', scale=4) }}.

La fonction return() préserve les types de données quand vous devez renvoyer des valeurs (dictionnaires, listes, entiers). Le contenu entre les balises de macro est rendu sous forme de chaîne de caractères : le SQL généré.

Pour les macros qui ont besoin de flexibilité sans explosion de paramètres, utilisez **kwargs :

{% macro flexible_macro(required_param, **kwargs) %}
{% set optional = kwargs.get('optional_param', 'default') %}
-- utiliser optional ici
{% endmacro %}

Objets de contexte que vous utiliserez constamment

dbt fournit plusieurs objets contenant des informations d’exécution :

ObjetUsagePropriétés courantes
thisRelation en cours de construction.database, .schema, .identifier
targetContexte de l’environnement cible.name, .type, .schema
modelMétadonnées du modèle courant.unique_id, .config, .tags
graphInformations sur les nœuds du projet.nodes, .sources (exécution uniquement)

Un pattern que vous utiliserez constamment est le flag execute. Il distingue la phase de parsing (construction du DAG) de la phase d’exécution (génération du SQL). Toute macro qui interroge la base de données a besoin de cette protection :

{% macro get_column_values(table, column) %}
{% if not execute %}
{{ return([]) }}
{% endif %}
{% set query %}
SELECT DISTINCT {{ column }} FROM {{ table }}
{% endset %}
{% set results = run_query(query) %}
{{ return(results.columns[0].values()) }}
{% endmacro %}

Sans la vérification execute, run_query() échouerait pendant le parsing quand la table n’existe pas encore. Le retour d’une liste vide pendant la phase de parsing satisfait dbt ; la vraie requête s’exécute ensuite.

Les macros essentielles pour chaque projet

Avant d’écrire des macros personnalisées, vérifiez si un package résout déjà votre problème. L’écosystème dbt offre d’excellentes options.

dbt-utils : le kit de démarrage

Le package dbt-utils (v1.3.3 début 2026) fournit des macros éprouvées pour les patterns courants.

generate_surrogate_key crée des clés hashées à partir de clés métier :

{{ dbt_utils.generate_surrogate_key(['customer_id', 'order_date']) }} AS order_key

star génère des listes de colonnes en excluant des colonnes spécifiques :

SELECT
{{ dbt_utils.star(from=ref('base__shopify__customers'), except=["_loaded_at", "_source"]) }}
FROM {{ ref('base__shopify__customers') }}

get_column_values permet du SQL dynamique basé sur les données réelles :

{% set methods = dbt_utils.get_column_values(
table=ref('base__shopify__payments'),
column='payment_method',
order_by='count(*) desc',
max_records=50
) %}

date_spine crée des séquences de dates continues pour l’analyse de séries temporelles :

{{ dbt_utils.date_spine(
datepart="day",
start_date="cast('2020-01-01' as date)",
end_date="cast('2026-12-31' as date)"
) }}

Qualité des données avec dbt-expectations

Le package dbt-expectations porte les tests de type Great Expectations vers dbt. Il est particulièrement utile pour les validations qui vont au-delà des tests de base :

columns:
- name: email
tests:
- dbt_expectations.expect_column_values_to_match_regex:
regex: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
- name: created_at
tests:
- dbt_expectations.expect_row_values_to_have_recent_data:
datepart: hour
interval: 3

Je couvre ce package en détail dans mon guide dbt-expectations. Utilisez-le quand vous avez besoin de validations par regex, de distributions statistiques ou de contrôles de fraîcheur que les tests intégrés ne couvrent pas.

Des patterns à adopter

Certains patterns apparaissent dans presque tous les projets dbt matures. En voici deux qui méritent d’être adoptés tôt.

Colonnes d’audit pour tracer les métadonnées ETL :

{% macro add_audit_columns() %}
CURRENT_TIMESTAMP() AS _loaded_at,
'{{ invocation_id }}' AS _dbt_invocation_id,
'{{ target.name }}' AS _dbt_target
{% endmacro %}

Appelez-la à la fin de votre select : SELECT *, {{ add_audit_columns() }} FROM ...

Limitation conditionnelle par environnement pour accélérer les runs de développement :

{% macro limit_data_in_dev(column_name='created_at', days=3) %}
{% if target.name == 'dev' %}
AND {{ column_name }} >= CURRENT_DATE() - {{ days }}
{% endif %}
{% endmacro %}

Ajoutez-la à vos clauses WHERE : WHERE 1=1 {{ limit_data_in_dev() }}. En dev, vous ne requêtez que trois jours de données. En prod, la clause disparaît.

Écrire des macros maintenables

Avoir des macros utiles, c’est une chose. Les garder maintenables à mesure que votre projet grandit en est une autre.

La règle de trois

Attendez qu’un bout de code apparaisse trois fois avant de l’abstraire. Première occurrence : écrivez-le en ligne. Deuxième occurrence : notez le pattern. Troisième occurrence : extrayez-le dans une macro.

La documentation officielle de dbt le résume bien : « Privilégiez la lisibilité quand vous mélangez Jinja et SQL, même si cela signifie répéter quelques lignes. »

La sur-abstraction a un coût réel : chaque couche d’indirection rend le débogage plus difficile, les listes de paramètres deviennent ingérables et les modifications de la macro cassent plusieurs chemins de code.

Principes de conception qui passent à l’échelle

Responsabilité unique : chaque macro ne devrait faire qu’une chose. Une macro qui convertit des devises, applique des remises et ajoute la taxe en fait trop. Séparez-la en cents_to_dollars, apply_discount et add_tax.

Nommage explicite : utilisez des préfixes verbaux qui indiquent ce que fait la macro. get_ récupère des données, generate_ crée une sortie, format_ transforme des chaînes. Préfixez les helpers internes avec un underscore : _build_filter_clause.

Paramètres raisonnables : visez cinq à sept paramètres maximum. Les paramètres obligatoires viennent en premier, sans valeur par défaut. Les paramètres optionnels ont des valeurs par défaut sensées. Quand vous avez besoin de plus de flexibilité, utilisez **kwargs plutôt que d’ajouter un quinzième paramètre.

Organisation du projet

Une structure de dossiers propre évite la prolifération des macros (voir aussi mon guide de structure de projet dbt) :

macros/
├── _macros.yml # Documentation de toutes les macros
├── generate_schema_name.sql # Macros d'override à la racine
├── utils/
│ ├── string_utils.sql
│ └── date_utils.sql
├── transformations/
│ ├── finance/
│ └── marketing/
└── tests/ # Tests génériques personnalisés

Le guide de style Brooklyn Data recommande une macro par fichier, avec le nom du fichier correspondant au nom de la macro. Les macros sont ainsi faciles à trouver et les fichiers restent ciblés.

Documentez vos macros dans _macros.yml :

macros:
- name: cents_to_dollars
description: |
Convertit un entier en cents vers un montant en dollars avec précision.
## Usage
```sql
{{ cents_to_dollars('amount_cents', scale=4) }}
```
arguments:
- name: column_name
type: column
description: Colonne contenant les cents
- name: scale
type: integer
description: Nombre de décimales. Par défaut 2.

Pour tester les macros, dbt 1.8+ a introduit les tests unitaires natifs :

unit_tests:
- name: test_currency_conversion
model: mrt__finance__orders
given:
- input: ref('base__shopify__orders')
rows:
- {order_id: 1, order__amount_cents: 1000}
expect:
rows:
- {order_id: 1, order__amount_dollars: 10.00}

Pièges courants et comment les éviter

Quelques erreurs reviennent régulièrement quand on apprend les macros.

Accolades imbriquées : les expressions Jinja à l’intérieur d’autres expressions n’ont pas besoin de doubles accolades.

{{ my_macro({{ var('x') }}) }} -- Incorrect
{{ my_macro(var('x')) }} -- Correct

Portée des variables dans les boucles : les variables définies dans une boucle ne persistent pas en dehors. Utilisez namespace pour les compteurs :

{% set ns = namespace(total=0) %}
{% for item in items %}
{% set ns.total = ns.total + item %}
{% endfor %}
{{ ns.total }} -- Fonctionne hors de la boucle

Garde execute manquante : toute macro qui appelle run_query() ou accède à graph.nodes a besoin d’une vérification execute. Sans elle, vous obtiendrez des erreurs de parsing cryptiques indiquant que les relations n’existent pas.

Déboguer à l’aveugle : quand une macro se comporte mal, vérifiez target/compiled/ pour voir le SQL rendu. Lancez dbt compile --select model_name pendant le développement. Utilisez {{ log("Debug: " ~ my_variable, info=true) }} pour inspecter les valeurs à l’exécution. Pour les erreurs fatales, {{ exceptions.raise_compiler_error("Invalid: " ~ value) }} arrête la compilation avec un message clair.

Pour commencer

Les macros deviennent plus faciles avec la pratique. Ajoutez dbt-utils et dbt-expectations à votre projet pour commencer :

packages.yml
packages:
- package: dbt-labs/dbt_utils
version: 1.3.3
- package: calogica/dbt_expectations
version: [">=0.10.0", "<0.11.0"]

Lancez dbt deps pour les installer, puis essayez generate_surrogate_key et star dans quelques modèles. Acquérir le réflexe d’appeler des macros aide avant d’en écrire soi-même.

Quand vous vous retrouvez à copier le même bloc SQL pour la troisième fois, c’est le signal pour l’extraire dans une macro avec un nom clair et une documentation appropriée. Des macros lisibles et simples vous serviront toujours mieux que des macros ingénieuses.