Projet dbt : modèles, sources et ref

Première leçon du cours Transformer les données avec dbt, en accès libre : environ 18 minutes, 4 exercices et 4 questions de quiz. C'est la leçon de base du catalogue ; dans l'espace étudiant, elle est réécrite pour ton profil.

Objectifs

  • Expliquer le rôle de dbt dans une chaîne ELT
  • Déclarer des sources et écrire des modèles de staging avec source()
  • Relier les modèles entre eux avec ref() et comprendre le graphe obtenu

Projet dbtModèleSourceFonction ref

Dans le cours précédent, vous avez écrit les couches nettoyée et métier d’un entrepôt en SQL. Avec des dizaines de requêtes qui dépendent les unes des autres, il faut un outil pour les ordonner, les tester, les documenter et les versionner. C’est le rôle de dbt (data build tool), un outil open source (dbt Core) en ligne de commande, également proposé en version hébergée par l’éditeur dbt Labs.

Ce que fait dbt

dbt s’occupe du T de l’ELT. Il ne charge pas de données depuis les applications (c’est le rôle d’un outil d’ingestion) et ne calcule rien lui-même : il compile vos fichiers SQL, enrichis de Jinja, puis les exécute dans l’entrepôt (PostgreSQL, BigQuery, Snowflake, Databricks, DuckDB, etc., via un adaptateur). Ce que vous écrivez reste donc du SQL.

Commandes essentielles :

  • dbt run : construit les modèles ;
  • dbt test : exécute les tests de données ;
  • dbt build : construit et teste dans l’ordre du graphe ;
  • dbt compile : produit le SQL final dans target/compiled/, sans l’exécuter.

La structure d’un projet

mon_projet/
├── dbt_project.yml        # nom du projet, dossiers, configuration par défaut
├── models/
│   ├── staging/shop/      # un dossier par source
│   │   ├── _shop__sources.yml
│   │   ├── stg_shop__orders.sql
│   │   └── stg_shop__customers.sql
│   └── marts/
│       └── customers.sql
├── macros/  tests/  seeds/  snapshots/

Les informations de connexion ne sont pas dans le projet : elles sont dans profiles.yml, hors du dépôt Git, avec des cibles (targets) comme dev et prod.

Déclarer les sources

Une source est une table que dbt n’a pas construite. On la déclare en YAML :

version: 2
sources:
  - name: shop
    schema: raw_shop
    tables:
      - name: orders
      - name: customers

Dans un modèle, on la lit avec {{ source('shop', 'orders') }}, que dbt remplace à la compilation par le nom complet, par exemple raw_shop.orders. Avantages : le nom physique n’est écrit qu’une fois, la source apparaît dans le graphe et la documentation, et l’on peut contrôler sa fraîcheur (dbt source freshness) en indiquant une colonne d’horodatage de chargement et des seuils d’alerte.

Écrire un modèle de staging

Un modèle est un fichier .sql qui contient une seule requête SELECT. Pas de CREATE TABLE : dbt l’ajoute selon la matérialisation choisie (leçon suivante). Le nom du fichier devient le nom de l’objet créé.

-- models/staging/shop/stg_shop__orders.sql
with source as (
    select * from {{ source('shop', 'orders') }}
),
renamed as (
    select
        id                         as order_id,
        user_id                    as customer_id,
        lower(status)              as status,
        amount_cents / 100.0       as amount,
        date(created_at)           as order_date
    from source
)
select * from renamed

Un modèle de staging : une source, des renommages cohérents (order_id plutôt que id), des types et unités harmonisés, aucune jointure ni règle métier. C’est l’équivalent de la couche nettoyée vue dans le cours DE1. Le style avec CTE nommées (source, renamed, final) est une convention répandue qui facilite la relecture.

ref() et le graphe

Un modèle en aval lit les modèles de staging avec ref() :

-- models/marts/customers.sql
select c.customer_id, c.first_name,
       count(o.order_id)          as nb_orders,
       coalesce(sum(o.amount), 0) as total_amount
from {{ ref('stg_shop__customers') }} as c
left join {{ ref('stg_shop__orders') }} as o
       on o.customer_id = c.customer_id and o.status <> 'returned'
group by c.customer_id, c.first_name

ref() fait deux choses :

  1. il résout le nom : en développement, dbt_lea.stg_shop__orders ; en production, analytics.stg_shop__orders. Le même code fonctionne dans chaque environnement ;
  2. il déclare une dépendance : dbt construit le graphe orienté sans cycle (DAG) de tous les modèles et les exécute dans le bon ordre, en parallèle quand c’est possible.

N’écrivez donc jamais en dur le nom d’une table construite par dbt : sans ref(), dbt ignore la dépendance, peut exécuter les modèles dans le mauvais ordre et lirait la table de production depuis votre environnement de développement.

Exercices

Les solutions et les tests restent sur le serveur. Dans l'espace étudiant, ton code est exécuté et corrigé, et le tuteur donne des indices avant la solution.

  1. CodeDifficulté 2SQL (SQLite) dans le navigateur2 indices

    Modèle stg_shop__orders compilé

    Tables sources (déclarées dans dbt comme source('shop', 'orders') et source('shop', 'customers')) : raw_orders(id, user_id, status, amount_cents, created_at) et raw_customers(id, first_name, last_name, email). Dans l’exercice, écrivez directement les noms de tables : c’est le SQL compilé par dbt.

    Écrivez la requête du modèle de staging des commandes, avec les colonnes :

    • order_id (depuis id) et customer_id (depuis user_id) ;
    • status en minuscules ;
    • amount : le montant en euros (amount_cents / 100.0) ;
    • order_date : la date seule, extraite de created_at avec date().
    Données de l'exercice (SQL)
    -- Tables brutes chargées par l'outil d'ingestion (schéma raw_shop)
    CREATE TABLE raw_orders (id INTEGER, user_id INTEGER, status TEXT, amount_cents INTEGER, created_at TEXT);
    INSERT INTO raw_orders VALUES
     (1, 1, 'COMPLETED', 4990, '2026-03-01 09:15:00'),
     (2, 2, 'completed', 12000, '2026-03-01 14:02:00'),
     (3, 1, 'Returned', 1990, '2026-03-02 10:30:00'),
     (4, 3, 'placed', 3450, '2026-03-03 18:45:00');
    CREATE TABLE raw_customers (id INTEGER, first_name TEXT, last_name TEXT, email TEXT);
    INSERT INTO raw_customers VALUES
     (1, 'Amina', 'Diallo', ' AMINA@EX.FR'), (2, 'Bastien', 'Morel', 'bastien@ex.fr'),
     (3, 'Camille', 'Roux', 'camille@ex.fr'), (4, 'Dina', 'Petit', 'dina@ex.fr');
    with source as (
        select * from raw_orders   -- {{ source('shop', 'orders') }} une fois compilé
    )
    select ...
    from source
  2. CodeDifficulté 2SQL (SQLite) dans le navigateur2 indices

    Modèle marts customers

    Modèles disponibles (compilés en tables) : stg_customers(customer_id, first_name, email) et stg_orders(order_id, customer_id, status, amount, order_date).

    Écrivez le modèle customers : pour chaque client (y compris ceux sans commande), affichez customer_id, first_name, nb_orders (commandes dont le statut n’est pas returned), total_amount (0 si aucune commande) et first_order_date.

    Données de l'exercice (SQL)
    CREATE TABLE stg_customers (customer_id INTEGER, first_name TEXT, email TEXT);
    INSERT INTO stg_customers VALUES (1, 'Amina', 'amina@ex.fr'), (2, 'Bastien', 'bastien@ex.fr'), (3, 'Camille', 'camille@ex.fr'), (4, 'Dina', 'dina@ex.fr');
    CREATE TABLE stg_orders (order_id INTEGER, customer_id INTEGER, status TEXT, amount REAL, order_date TEXT);
    INSERT INTO stg_orders VALUES
     (1, 1, 'completed', 49.9, '2026-03-01'), (2, 2, 'completed', 120.0, '2026-03-01'),
     (3, 1, 'returned', 19.9, '2026-03-02'), (4, 3, 'placed', 34.5, '2026-03-03');
    select c.customer_id, c.first_name,
           count(o.order_id) as nb_orders,
           ... as total_amount,
           ... as first_order_date
    from stg_customers as c   -- {{ ref('stg_shop__customers') }}
    left join stg_orders as o on ...
    group by c.customer_id, c.first_name
  3. QCMDifficulté 11 indice

    Le rôle de ref()

    Que se passe-t-il si un modèle lit analytics.stg_shop__orders écrit en dur au lieu de {{ ref('stg_shop__orders') }} ?

    • Rien, c'est équivalent
    • dbt ne connaît pas la dépendance : l'ordre d'exécution peut être faux et le développement lit la table de production
    • dbt refuse de compiler le projet
  4. TâcheDifficulté 22 indices

    Déclarer les sources et le premier modèle

    Dans un projet dbt nouvellement créé (dbt init), les tables raw_shop.orders et raw_shop.customers existent dans l’entrepôt. Créez :

    1. le fichier models/staging/shop/_shop__sources.yml qui déclare la source shop (schéma raw_shop) et ses deux tables ;
    2. le modèle models/staging/shop/stg_shop__customers.sql qui renomme id en customer_id et normalise l’email (minuscules, sans espaces) ;
    3. exécutez dbt run --select stg_shop__customers, puis dbt compile et ouvrez le SQL compilé.
    • Le fichier YAML commence par version: 2 et déclare sources > shop > tables orders et customers
    • Le modèle lit la table avec {{ source('shop', 'customers') }} et non avec un nom en dur
    • Le modèle ne contient qu'une requête SELECT, sans CREATE
    • dbt run se termine avec 1 modèle réussi (PASS=1)
    • Le fichier compilé dans target/compiled/ contient raw_shop.customers à la place de l'appel source()

Quiz de fin de leçon

  1. Quelle partie de l’ELT dbt prend-il en charge ?

    • L'extraction depuis les applications
    • Le chargement
    • La transformation dans l'entrepôt
  2. Que contient un fichier de modèle dbt ?

    • Une instruction CREATE TABLE
    • Une seule requête SELECT
    • Un script Python obligatoire
  3. Comment lire une table chargée par l’outil d’ingestion ?

    • {{ ref('orders') }}
    • {{ source('shop', 'orders') }}
    • {{ table('orders') }}
  4. Où se trouvent les identifiants de connexion à l’entrepôt ?

    • Dans dbt_project.yml
    • Dans profiles.yml, hors du dépôt
    • Dans chaque modèle

La suite du cours

  1. Jinja, macros et matérialisations18 min
  2. Tester les données18 min
  3. Modèles incrémentaux et snapshots20 min
  4. Documentation, lignage et bonnes pratiques18 min

Transformer les données avec dbt · Intermédiaire · 1 h 32