Transformer vos données brutes en informations fiables. Maîtrisez le « T » du Modern Data Stack : modélisation, tests et documentation.
dbt (Data Build Tool) est un outil open-source qui permet de transformer les données directement dans votre data warehouse en écrivant du SQL. C'est le « T » (Transform) du pipeline ELT. dbt ne charge pas les données (c'est le rôle d'outils comme Meltano, Fivetran, ou Airbyte) — il les transforme une fois qu'elles sont dans le warehouse.
Imaginez une cuisine industrielle. Les ingrédients bruts (données sources) arrivent par camion (extraction). Ils sont stockés dans un immense frigo (data warehouse). dbt, c'est le chef cuisinier : il prend les ingrédients bruts et les transforme en plats finis (tableaux de bord, rapports, métriques) en suivant des recettes (modèles SQL). Et comme un bon chef, il documente ses recettes, teste la qualité des plats, et organise sa cuisine de manière reproductible.
| Critère | dbt Core (Open-Source) | dbt Cloud (SaaS) |
|---|---|---|
| Prix | Gratuit | À partir de $100/mois |
| Exécution | En local ou votre CI/CD | Hébergé par dbt Labs |
| IDE | Votre éditeur favori | IDE web intégré |
| Scheduler | Cron, Airflow, Dagster, Prefect | Intégré |
| Documentation | Générée localement | Hébergée automatiquement |
| CI/CD | À configurer vous-même | Slim CI intégré |
| Idéal pour | Équipes techniques, open-source | Équipes mixtes, onboarding rapide |
dbt s'inscrit dans le Modern Data Stack, une architecture data modulaire où chaque outil a une responsabilité claire :
| Étape | Rôle | Outils |
|---|---|---|
| Extract | Récupérer les données des sources | Meltano, Fivetran, Airbyte, Stitch |
| Load | Charger dans le warehouse | Meltano, Fivetran, Airbyte |
| Transform | Transformer les données brutes en modèles analytiques | dbt ⭐ |
| Warehouse | Stocker et requêter les données | Snowflake, BigQuery, Redshift, DuckDB, Postgres |
| Orchestrate | Planifier et orchestrer les pipelines | Airflow, Dagster, Prefect, dbt Cloud |
| Visualize | Créer des dashboards | Looker, Metabase, Superset, Evidence |
| Quality | Surveiller la qualité des données | dbt tests, Great Expectations, Soda, Elementary |
L'ancien paradigme ETL (Extract-Transform-Load) transformait les données avant de les charger, ce qui demandait des outils complexes (Informatica, Talend). Le nouveau paradigme ELT charge d'abord les données brutes, puis les transforme dans le warehouse. Avantage : le warehouse moderne est assez puissant pour faire les transformations, et vous gardez toujours les données brutes.
Un modèle dbt est simplement un fichier SQL avec une requête SELECT. dbt
exécute ce SELECT et matérialise le résultat comme une table ou une vue dans le warehouse.
-- models/staging/stg_orders.sql
SELECT
id AS order_id,
user_id AS customer_id,
order_date,
status,
amount / 100.0 AS amount_eur -- centimes → euros
FROM {{ source('raw', 'orders') }}
WHERE status != 'cancelled'
Ce modèle prend les commandes brutes, renomme les colonnes, convertit les montants, et filtre les annulées.
Les sources déclarent les tables brutes qui arrivent dans votre warehouse (via Meltano, Fivetran, etc.). Elles sont déclarées dans un fichier YAML :
# models/staging/_sources.yml
version: 2
sources:
- name: raw
database: my_warehouse
schema: raw_data
tables:
- name: orders
description: "Commandes brutes depuis Shopify"
loaded_at_field: _loaded_at
freshness:
warn_after: { count: 12, period: hour }
error_after: { count: 24, period: hour }
| Type | Comportement | Quand l'utiliser |
|---|---|---|
| view | Crée une vue SQL (pas de stockage) | Modèles légers, staging, données toujours fraîches |
| table | Crée une vraie table (stockage physique) | Modèles lourds, agrégations, marts finaux |
| incremental | N'ajoute que les nouvelles lignes | Tables volumineuses (logs, événements, transactions) |
| ephemeral | CTE inlinée (pas de table/vue) | Sous-requêtes réutilisables, pas de matérialisation |
dbt intègre des tests de qualité des données directement dans votre pipeline :
# models/staging/_schema.yml
version: 2
models:
- name: stg_orders
columns:
- name: order_id
tests:
- unique
- not_null
- name: status
tests:
- accepted_values:
values: ['pending', 'shipped', 'delivered', 'returned']
- name: customer_id
tests:
- relationships:
to: ref('stg_customers')
field: customer_id
4 tests génériques natifs : unique, not_null,
accepted_values, relationships. Vous pouvez aussi écrire des tests SQL
custom.
dbt utilise Jinja2 comme moteur de template, ce qui rend le SQL dynamique et réutilisable :
-- Référencer un autre modèle
SELECT * FROM {{ ref('stg_orders') }}
-- Référencer une source
SELECT * FROM {{ source('raw', 'orders') }}
-- Condition selon l'environnement
{% if target.name == 'prod' %}
WHERE created_at >= '2024-01-01'
{% endif %}
-- Boucle dynamique
{% set payment_methods = ['credit_card', 'paypal', 'bank'] %}
{% for method in payment_methods %}
SUM(CASE WHEN payment_method = '{{ method }}'
THEN amount ELSE 0 END) AS {{ method }}_amount
{% if not loop.last %},{% endif %}
{% endfor %}
La convention la plus importante de dbt est l'architecture en 3 couches :
models/
├── staging/ # Couche 1 : Nettoyage
│ ├── shopify/
│ │ ├── _shopify__sources.yml
│ │ ├── _shopify__models.yml
│ │ ├── stg_shopify__orders.sql
│ │ └── stg_shopify__customers.sql
│ └── stripe/
│ ├── _stripe__sources.yml
│ ├── stg_stripe__payments.sql
│ └── stg_stripe__refunds.sql
├── intermediate/ # Couche 2 : Logique métier
│ ├── int_orders_with_payments.sql
│ └── int_customer_lifetime.sql
└── marts/ # Couche 3 : Tables finales
├── finance/
│ ├── fct_revenue.sql # Fact table
│ └── dim_customers.sql # Dimension table
├── marketing/
│ ├── fct_campaigns.sql
│ └── dim_channels.sql
└── product/
└── fct_user_activity.sql
| Préfixe | Couche | Exemple | Règle |
|---|---|---|---|
stg_ |
Staging | stg_shopify__orders |
1:1 avec la source, renommage + cast |
int_ |
Intermediate | int_orders_with_payments |
Jointures, logique métier interne |
fct_ |
Marts (facts) | fct_revenue |
Tables de faits (événements, transactions) |
dim_ |
Marts (dimensions) | dim_customers |
Tables de dimensions (entités, référentiels) |
-- models/marts/finance/fct_daily_revenue.sql
WITH orders AS (
SELECT * FROM {{ ref('stg_shopify__orders') }}
),
payments AS (
SELECT * FROM {{ ref('stg_stripe__payments') }}
)
SELECT
o.order_date,
COUNT(DISTINCT o.order_id) AS total_orders,
COUNT(DISTINCT o.customer_id) AS unique_customers,
SUM(p.amount) AS gross_revenue,
SUM(p.amount) - SUM(p.fees) AS net_revenue,
SUM(p.amount) / COUNT(DISTINCT o.order_id) AS avg_order_value
FROM orders o
LEFT JOIN payments p ON o.order_id = p.order_id
GROUP BY o.order_date
Ce modèle alimente directement un dashboard Metabase/Looker avec les KPIs de vente.
-- models/marts/finance/fct_mrr.sql
WITH subscriptions AS (
SELECT * FROM {{ ref('int_active_subscriptions') }}
)
SELECT
date_trunc('month', period_start) AS month,
SUM(CASE WHEN type = 'new' THEN mrr ELSE 0 END) AS new_mrr,
SUM(CASE WHEN type = 'expansion' THEN mrr ELSE 0 END) AS expansion_mrr,
SUM(CASE WHEN type = 'contraction' THEN mrr ELSE 0 END) AS contraction_mrr,
SUM(CASE WHEN type = 'churn' THEN mrr ELSE 0 END) AS churned_mrr,
SUM(mrr) AS total_mrr
FROM subscriptions
GROUP BY 1
-- models/marts/marketing/fct_attribution.sql
SELECT
campaign_id,
channel,
SUM(first_touch_revenue) AS first_touch_attributed,
SUM(last_touch_revenue) AS last_touch_attributed,
SUM(linear_revenue) AS linear_attributed,
COUNT(DISTINCT customer_id) AS customers_acquired,
SUM(spend) AS total_spend,
SUM(linear_revenue) / NULLIF(SUM(spend), 0) AS roas
FROM {{ ref('int_touchpoints_attributed') }}
GROUP BY campaign_id, channel
-- models/intermediate/int_bank_reconciliation.sql
SELECT
b.transaction_id,
b.amount AS bank_amount,
e.amount AS erp_amount,
ABS(b.amount - e.amount) AS difference,
CASE
WHEN e.transaction_id IS NULL THEN 'unmatched_bank'
WHEN ABS(b.amount - e.amount) > 0.01 THEN 'amount_mismatch'
ELSE 'matched'
END AS reconciliation_status
FROM {{ ref('stg_bank__transactions') }} b
FULL OUTER JOIN {{ ref('stg_erp__transactions') }} e
ON b.transaction_id = e.transaction_id
| Phase | Commande dbt | Ce qui se passe |
|---|---|---|
| 1. Init | dbt init mon_projet |
Crée la structure de projet avec les dossiers et fichiers de config |
| 2. Développement | dbt run --select stg_orders |
Compile et exécute un modèle spécifique dans le warehouse |
| 3. Test | dbt test |
Exécute tous les tests de qualité des données |
| 4. Documentation | dbt docs generate && dbt docs serve |
Génère et sert la documentation interactive (avec DAG) |
| 5. Build complet | dbt build |
Exécute run + test + snapshot en une seule commande |
| 6. Deploy | dbt run --target prod |
Exécute tous les modèles dans l'environnement de production |
| 7. Freshness | dbt source freshness |
Vérifie que les sources sont à jour (pas de données stale) |
dbt construit automatiquement un graphe de dépendances entre vos modèles grâce aux
fonctions ref() et source(). Ce DAG garantit que les modèles sont exécutés
dans le bon ordre et permet de ne relancer que ce qui est nécessaire.
Le DAG de dbt est l'un de ses plus grands avantages. Il permet de visualiser toute la chaîne de
transformation, d'identifier les dépendances circulaires, et d'exécuter des sous-ensembles du
pipeline avec les sélecteurs (dbt run --select +fct_revenue pour exécuter fct_revenue
et tous ses ancêtres).
Les snapshots capturent l'historique des changements dans vos données sources :
-- snapshots/snap_customers.sql
{% snapshot snap_customers %}
{{
config(
target_schema='snapshots',
unique_key='customer_id',
strategy='timestamp',
updated_at='updated_at'
)
}}
SELECT * FROM {{ source('raw', 'customers') }}
{% endsnapshot %}
Chaque fois qu'un client change d'adresse ou de plan, dbt crée une nouvelle ligne avec des colonnes
dbt_valid_from et dbt_valid_to.
-- models/marts/fct_events.sql
{{
config(
materialized='incremental',
unique_key='event_id',
incremental_strategy='merge'
)
}}
SELECT
event_id,
user_id,
event_type,
created_at
FROM {{ ref('stg_events') }}
{% if is_incremental() %}
WHERE created_at > (SELECT MAX(created_at) FROM {{ this }})
{% endif %}
Au lieu de recréer la table entière à chaque run (qui peut prendre des heures sur des milliards de lignes), le modèle incrémental ne traite que les nouvelles données.
# packages.yml
packages:
- package: dbt-labs/dbt_utils # Utilitaires SQL essentiels
version: [">=1.0.0", "<2.0.0"]
- package: dbt-labs/codegen # Génération automatique de staging
version: [">=0.12.0", "<1.0.0"]
- package: calogica/dbt_expectations # Tests avancés (regex, ranges, distributions)
version: [">=0.10.0", "<1.0.0"]
- package: elementary-data/elementary # Monitoring & observabilité
version: [">=0.13.0", "<1.0.0"]
Les seeds sont des fichiers CSV versionnés dans git pour les données de référence statiques :
# seeds/country_codes.csv
code,name,region
FR,France,Europe
US,United States,North America
JP,Japan,Asia Pacific
# Puis dans un modèle :
SELECT * FROM {{ ref('country_codes') }}
# models/_exposures.yml
exposures:
- name: weekly_revenue_dashboard
type: dashboard
maturity: high
url: https://metabase.company.com/dashboard/42
description: "Dashboard hebdomadaire des revenus"
depends_on:
- ref('fct_daily_revenue')
- ref('dim_customers')
owner:
name: Équipe Finance
email: [email protected]
Le pattern le plus classique en data warehousing : une table de faits centrale entourée de tables de dimensions.
| Type | Préfixe | Contient | Exemple |
|---|---|---|---|
| Fact (Faits) | fct_ |
Événements, transactions, mesures | fct_orders, fct_payments |
| Dimension | dim_ |
Entités, attributs descriptifs | dim_customers, dim_products |
Alternative au Star Schema pour les cas simples : une seule table dénormalisée avec toutes les informations. Plus simple pour les analystes, moins flexible pour les cas complexes.
Pattern émergent pour les données événementielles : toutes les activités d'un utilisateur dans une seule
table avec un schéma flexible (activity_type, feature_1,
feature_2, etc.).
-- Créer une clé de substitution avec dbt_utils
SELECT
{{ dbt_utils.generate_surrogate_key(['order_id', 'product_id']) }}
AS order_line_id,
order_id,
product_id,
quantity,
unit_price
FROM {{ ref('stg_order_lines') }}
-- Combiner plusieurs sources du même type
WITH shopify AS (
SELECT *, 'shopify' AS source_system
FROM {{ ref('stg_shopify__orders') }}
),
amazon AS (
SELECT *, 'amazon' AS source_system
FROM {{ ref('stg_amazon__orders') }}
)
SELECT * FROM shopify
UNION ALL
SELECT * FROM amazon
unique et not_null sur sa clé primaire. Les marts doivent avoir des
tests métier.
source().{{ ref('model_name') }} pour que le DAG soit
complet.freshness sur vos sources pour détecter quand les données ne sont plus à jour.
Un seul fichier SQL de 500 lignes qui fait staging + joins + agrégations. Impossible à tester, débugger, ou réutiliser. Découpez en couches !
Référencer directement une source depuis un mart au lieu de passer par le staging. Vous perdez la couche de nettoyage et le DAG devient incohérent.
Pas de tests = pas de confiance dans vos données. Un dashboard qui affiche des chiffres faux est pire que pas de dashboard du tout.
Ne jamais utiliser SELECT * en production. Listez explicitement les colonnes pour la
lisibilité, la performance, et la détection de changements de schéma.
Si votre calcul de MRR est dans Looker/Metabase au lieu d'être dans dbt, il n'est ni testé, ni versionné, ni réutilisable. Centralisez la logique dans dbt.
Ne rendez un modèle incrémental que quand c'est nécessaire (performance). C'est plus complexe à
maintenir et plus facile à casser qu'un simple table.
| Critère | Star Schema | Wide Table |
|---|---|---|
| Complexité | Plus élevée (plusieurs tables) | Faible (une seule table) |
| Performance BI | Excellente (jointures optimisées) | Bonne (pas de jointures) |
| Réutilisabilité | Élevée (dimensions partagées) | Faible (monolithique) |
| Quand choisir | Plusieurs cas d'usage, équipe data | Un seul dashboard, petit projet |
dbt run prend plus de 30 minutesCommencez avec dbt Core si vous avez des compétences techniques et un orchestrateur existant (Airflow, Dagster). Passez à dbt Cloud si votre équipe grandit et que vous voulez un IDE web, un scheduler intégré, et un Slim CI sans configuration.
# profiles.yml
my_project:
target: dev # Environnement par défaut
outputs:
dev:
type: postgres
schema: dev_{{ env_var('USER') }} # Schéma isolé par dev
staging:
type: postgres
schema: staging
prod:
type: postgres
schema: analytics # Le schéma final
Chaque développeur travaille dans son propre schéma (dev_alice, dev_bob).
Le staging est partagé. La prod est protégée par CI/CD.
| Package | Usage | Incontournable ? |
|---|---|---|
dbt_utils |
Macros essentielles (surrogate keys, pivot, unpivot, date spine) | ✅ Oui |
dbt_expectations |
Tests avancés (regex, distributions, ranges) | ✅ Oui |
codegen |
Génération automatique de staging et YAML | ✅ Pour démarrer |
elementary |
Monitoring, anomaly detection, alertes Slack | ⭐ Recommandé |
dbt_date |
Dimension dates pré-construite | ⭐ Utile |
audit_helper |
Comparer deux versions d'un modèle après refactoring | ⭐ Pour les migrations |
pip install dbt-core dbt-postgres (ou dbt-snowflake, dbt-bigquery)dbt init mon_projetprofiles.yml avec vos credentials warehousedbt run && dbt testdbt docs generate && dbt docs serve