ServicesÀ proposNotesContact Me contacter →
EN FR
Note

Jinja Templating pour les praticiens SQL

Pourquoi Jinja est naturel pour les analytics engineers SQL-first — le modèle à double accolade, les macros comme helpers SQL, et la séparation des responsabilités qui garde les fichiers de transformation focalisés.

Planté
dbtdata engineeringdata modeling

Jinja est le moteur de templating de dbt, conçu à l’origine pour générer du HTML dans des frameworks web Python comme Flask et Django. Le même mécanisme qui génère des balises <ul> à partir d’une liste génère des colonnes SUM(CASE WHEN ...) à partir d’une liste de méthodes de paiement. La syntaxe est également standard dans les playbooks Ansible.

Jinja traite le templating comme une préoccupation distincte de la programmation. Le modèle est du SQL avec des variables, pas un programme qui émet des chaînes SQL. Les analytics engineers issus du SQL trouvent généralement cela plus facile à adopter que les alternatives basées sur JavaScript.

Le modèle à deux délimiteurs

Jinja utilise deux types de délimiteurs qui couvrent tout ce dont vous avez besoin :

{{ expression }} — évalue et affiche une valeur. C’est ici que vous mettez les références, les variables et les appels de fonctions :

SELECT customer_id, email
FROM {{ ref('base__stripe__customers') }}
WHERE status = {{ var('active_status') }}

{% statement %} — contrôle la logique sans produire d’output. Les conditionnelles, les boucles et les assignations de variables se trouvent ici :

{% if target.name == 'prod' %}
AND created_at > CURRENT_DATE - 90
{% endif %}
{% set payment_methods = ['bank_transfer', 'credit_card', 'gift_card'] %}
{% for method in payment_methods %}
SUM(CASE WHEN payment_method = '{{ method }}' THEN amount END) AS {{ method }}_amount
{{ ',' if not loop.last }}
{% endfor %}

Voilà l’intégralité du modèle mental. Deux types de délimiteurs. L’un pour l’output, l’autre pour la logique. Vous pouvez apprendre Jinja suffisamment pour dbt en une après-midi.

Comparons avec l’approche JavaScript de Dataform, où la même boucle ressemble à :

${["bank_transfer", "credit_card", "gift_card"]
.map(method => `SUM(CASE WHEN payment_method = '${method}' THEN amount END) AS ${method}_amount`)
.join(", ")}

Les deux produisent un SQL identique. La version Jinja nécessite de savoir ce que font {% for %} et loop.last. La version JavaScript nécessite de connaître .map(), les fonctions fléchées, les template literals et .join(). Pour quelqu’un qui écrit du SQL quotidiennement et du JavaScript occasionnellement, la version Jinja est plus facile à relire un mois plus tard.

Les macros comme helpers SQL

L’analogie qui parle aux praticiens SQL : les macros Jinja sont comme des fonctions SQL, sauf qu’elles s’exécutent à la compilation et retournent du texte SQL plutôt que des résultats de requête.

Une fonction SQL :

CREATE FUNCTION cents_to_dollars(cents INT64)
RETURNS FLOAT64 AS (ROUND(cents / 100.0, 2));

La macro Jinja équivalente :

{% macro cents_to_dollars(column_name) %}
ROUND({{ column_name }} / 100.0, 2)
{% endmacro %}

Appelée dans un modèle :

SELECT
order_id,
{{ cents_to_dollars('amount_cents') }} AS order__amount_dollars
FROM {{ ref('base__stripe__orders') }}

La macro se résout à la compilation et l’entrepôt voit :

SELECT
order_id,
ROUND(amount_cents / 100.0, 2) AS order__amount_dollars
FROM analytics.base__stripe__orders

Ce pattern maintient les macros comme des helpers SQL. Vous n’écrivez pas un programme qui génère du SQL — vous paramétrez un snippet de SQL. L’intention est transparente ; le comportement est prévisible.

Un exemple légèrement plus élaboré — une macro pour extraire les paramètres d’événements GA4, qui nécessite le même pattern UNNEST sur chaque modèle :

{% macro unnest_event_param(param_name, value_type='string_value') %}
(SELECT value.{{ value_type }}
FROM UNNEST(event_params)
WHERE key = '{{ param_name }}')
{% endmacro %}

Dans un modèle :

SELECT
event_id,
{{ unnest_event_param('page_location') }} AS page_location,
{{ unnest_event_param('session_id') }} AS session_id,
{{ unnest_event_param('ga_session_number', 'int_value') }} AS session_number
FROM {{ ref('base__ga4__events') }}

N’importe quel praticien SQL peut lire ceci et le comprendre. La macro masque la syntaxe UNNEST répétitive sans masquer l’intention.

L’équivalent dans Dataform est une fonction JavaScript dans un fichier includes/ :

function unnestEventParam(paramName, valueType = 'string_value') {
return `(SELECT value.${valueType}
FROM UNNEST(event_params)
WHERE key = '${paramName}')`;
}
module.exports = { unnestEventParam };

Les deux approches fonctionnent. Aucune n’a d’avantage matériel pour des helpers simples comme celui-ci. La version Jinja s’intègre directement au système de macros de dbt et est appelable depuis n’importe quel modèle sans instructions d’import. La version JavaScript nécessite module.exports et des imports explicites dans les fichiers consommateurs.

Séparation des responsabilités via YAML

Un aspect de l’approche Jinja de dbt que les analytics engineers apprécient souvent : le fichier SQL se concentre sur la transformation, pas sur les métadonnées.

Un modèle dbt :

-- models/marts/mrt__stripe__customers.sql
{{ config(
materialized='table',
schema='marts'
) }}
SELECT
customer_id,
email,
{{ cents_to_dollars('lifetime_value_cents') }} AS lifetime_value,
created_at
FROM {{ ref('int__stripe__customers_enriched') }}

Les tests, les descriptions de colonnes et les assertions de qualité des données se trouvent séparément dans un fichier YAML :

models/marts/_marts__models.yml
models:
- name: mrt__stripe__customers
description: "Une ligne par client avec le statut de facturation actuel et la valeur vie."
columns:
- name: customer_id
tests:
- unique
- not_null
- name: email
tests:
- not_null

Comparez avec le SQLX de Dataform, où tout coexiste dans un seul fichier :

config {
type: "table",
schema: "marts",
assertions: {
uniqueKey: ["customer_id"],
nonNull: ["customer_id", "email"]
}
}
SELECT
customer_id,
email,
ROUND(lifetime_value_cents / 100.0, 2) AS lifetime_value,
created_at
FROM ${ref("int__stripe__customers_enriched")}

Aucune disposition n’est objectivement meilleure. L’approche tout-en-un de Dataform convient aux ingénieurs qui préfèrent tout voir en un seul endroit. Le découpage de dbt convient aux équipes qui souhaitent la logique de transformation dans les fichiers SQL et la logique de qualité des données dans les fichiers YAML — deux réviseurs différents, deux préoccupations différentes, deux fichiers différents.

En pratique, les tests YAML de dbt deviennent un avantage significatif à mesure que l’écosystème de tests se développe. Ajouter des tests dbt-expectations ou elementary au YAML ne nécessite aucune modification du fichier SQL. Voir les limites des tests Dataform pour la comparaison complète de ce que chaque écosystème fournit.

L’écosystème de packages mature

Un avantage pratique qui découle de l’adoption de Jinja dans la communauté dbt : une large bibliothèque de macros préconstruites qui fonctionnent dans n’importe quel projet dbt.

dbt-utils en est l’exemple canonique. Il fournit :

  • generate_surrogate_key — clés composites hashées avec une gestion cohérente des nulls
  • star — génération de listes de colonnes avec exclusions
  • get_column_values — SQL dynamique basé sur les données réelles
  • date_spine — séquences de dates continues pour les lacunes de séries temporelles
  • union_relations — combinaison de tables avec des schémas différents

Aucun de ceux-ci ne nécessite d’écrire du Jinja personnalisé. Ils sont installables dans packages.yml et immédiatement disponibles sous la forme {{ dbt_utils.generate_surrogate_key(['customer_id', 'order_date']) }}.

L’équivalent dans Dataform consiste à construire chaque utilitaire à partir de zéro en tant que fonction JavaScript. L’écosystème de packages communautaires pour Dataform est d’un ordre de grandeur inférieur. Pour les équipes qui s’attendent à utiliser des patterns standardisés d’analytics engineering — et la plupart le font — cette asymétrie est significative.

Quand la contrainte devient visible

La facilité d’accès de Jinja pour les praticiens SQL est réelle, mais ses limites le sont aussi. La contrainte apparaît lorsque vous avez besoin de générer des modèles de manière programmatique.

Jinja s’exécute à l’intérieur d’un seul fichier .sql et génère une seule instruction SQL. Il ne peut pas créer de nouveaux fichiers, de nouveaux modèles, ni de nouveaux nœuds dans le DAG. Si vous avez besoin de 50 versions spécifiques à un pays du même modèle, la réponse de Jinja est : écrire 50 fichiers, ou trouver un contournement. La réponse de JavaScript est : écrire une boucle.

Pour la plupart des projets d’analytics engineering, ce plafond n’est jamais atteint. La modélisation dimensionnelle standard — modèles base, jointures intermédiaires, agrégations mart — ne nécessite pas de génération dynamique de modèles. Jinja la gère confortablement. Le pattern de génération programmatique de modèles est véritablement puissant, mais la plupart des équipes n’en ont pas besoin.

Si un projet a besoin de génération dynamique de modèles, la contrainte est réelle. Si ce n’est pas le cas, la facilité d’accès de Jinja pour les praticiens SQL est un avantage pratique.