Dans l'article précédent, tu as installé dbt, connecté Snowflake et fait tourner un premier modèle. Mais ce modèle, c'était un SELECT avec des valeurs écrites en dur. Sympa pour vérifier que tout marche mais inutile en vrai.

Maintenant on travaille sur de vraies données, en disant à dbt où elles se trouvent (les sources), puis à construire la première couche propre par-dessus (le staging). C'est le socle de tout projet dbt, et ça correspond pile au passage Bronze vers Silver de l'architecture Médaillon.

D'abord, des données brutes dans Snowflake

Pour déclarer des sources, il faut bien que quelque chose existe à la source. Dans un vrai projet, ces données arrivent d'un outil d'ingestion comme par exemple (Fivetran, Airbyte, Talend, Snowpipe) et atterrissent dans un schéma brut. On va simuler ça à la main.

Ouvre une feuille SQL dans Snowsight et crée un schéma raw avec deux tables :

USE ROLE dbt_role;
USE DATABASE analytics;

-- Le schéma qui reçoit les données brutes (la couche Bronze)
CREATE SCHEMA IF NOT EXISTS raw;

-- Table clients
CREATE OR REPLACE TABLE raw.customers (
    id          INT,
    first_name  STRING,
    last_name   STRING,
    email       STRING,
    created_at  TIMESTAMP,
    _loaded_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP()   -- date d'arrivée de la donnée
);

INSERT INTO raw.customers (id, first_name, last_name, email, created_at) VALUES
(1, 'Marc',   'Dubois', 'marc.dubois@email.fr',   '2026-01-10'),
(2, 'Sophie', 'Martin', 'sophie.martin@email.fr', '2026-01-12'),
(3, 'Karim',  'Benali', 'karim.benali@email.fr',  '2026-02-01');

-- Table commandes
CREATE OR REPLACE TABLE raw.orders (
    order_id     INT,
    customer_id  INT,
    amount       NUMBER(10,2),
    status       STRING,
    order_date   DATE,
    _loaded_at   TIMESTAMP DEFAULT CURRENT_TIMESTAMP()
);

INSERT INTO raw.orders (order_id, customer_id, amount, status, order_date) VALUES
(101, 1, 49.90, 'completed', '2026-02-15'),
(102, 1, 19.90, 'completed', '2026-03-02'),
(103, 2, 99.00, 'pending',   '2026-03-10'),
(104, 3, 14.99, 'completed', '2026-03-12');

Voilà ta couche brute. Des données telles qu'elles arrivent. La colonne _loaded_at nous servira plus loin pour la fraîcheur.

Déclarer les sources

On pourra écrire FROM analytics.raw.customers directement dans les modèles mais c'est une très mauvaise idée. Le jour où le schéma change de nom, tu dois modifier tous tes fichiers un par un. dbt règle ça avec les sources : tu déclares une fois où se trouvent les données brutes, et tu y fais référence par un nom logique.

Dans ton projet, crée un dossier models/staging/ et dedans un fichier _sources.yml :

version: 2

sources:
  - name: raw
    database: analytics
    schema: raw
    tables:
      - name: customers
      - name: orders

Tu viens de dire à dbt : "la source raw pointe vers analytics.raw, et elle contient les tables customers et orders". À partir de là, dans tes modèles, tu n'écris plus jamais le chemin physique. Tu écris :

SELECT * FROM {{ source('raw', 'customers') }}

dbt traduit ça tout seul en analytics.raw.customers au moment de l'exécution. Si demain la source change, on va changer une ligne dans le YAML et c'est réglé partout.

La couche staging

La couche staging, c'est la première transformation. Une règle simple : un modèle de staging par table source, et on y fait du nettoyage léger seulement. Renommer les colonnes, caster les types, harmoniser les formats. Pas de jointure, pas d'agrégat, pas de logique métier. Ça vient après.

Crée models/staging/stg_customers.sql :

with source as (
    select * from {{ source('raw', 'customers') }}
),

renamed as (
    select
        id          as customer_id,
        first_name  as prenom,
        last_name   as nom,
        lower(email) as email,
        created_at  as date_creation
    from source
)

select * from renamed

Puis models/staging/stg_orders.sql :

with source as (
    select * from {{ source('raw', 'orders') }}
),

renamed as (
    select
        order_id,
        customer_id,
        amount      as montant,
        status      as statut,
        order_date  as date_commande
    from source
)

select * from renamed

Le pattern with source as (...) , renamed as (...) est une convention dbt très répandue. Ça rend le modèle lisible car on voit d'un coup d'où vient la donnée et ce qu'on en fait. Si les CTE ne te parlent pas, j'en ai fait un article dédié.

Le nommage stg_

💡
Tous tes modèles de staging commencent par stg_. C'est une norme de la communauté dbt et pas une obligation technique. Mais elle te sauve la vie quand ton projet grossit car d'un coup d'oeil tu sais qu'un modèle stg_orders est une couche d'entrée brute, pas un modèle métier. On range même chaque système source dans son sous-dossier : staging/stripe/, staging/salesforce/ et on ne mélange pas tout dans la racine

ref() pour chaîner les modèles

Tu sais référencer une source avec source(). Pour référencer un autre modèle dbt, tu utilises ref(). Et c'est le coeur de dbt car c'est avec cela qu'il construit le graphe de dépendances et qui détermine dans quel ordre tout s'exécute.

Crée un dossier models/marts/ et un modèle mart_clients_commandes.sql qui combine tes deux modèles de staging :

with clients as (
    select * from {{ ref('stg_customers') }}
),

commandes as (
    select * from {{ ref('stg_orders') }}
)

select
    c.customer_id,
    c.prenom,
    c.nom,
    count(o.order_id)   as nb_commandes,
    sum(o.montant)      as total_depense
from clients c
left join commandes o
    on c.customer_id = o.customer_id
group by 1, 2, 3

Remarque qu'on n'écrit jamais le nom physique d'une table. On passe par ref('stg_customers'), jamais par analytics.dev.stg_customers. C'est ça qui permet à dbt de savoir que `mart_clients_commandes` dépend de stg_customers et stg_orders, et donc de les construire avant.

Lance tout :

dbt run

dbt construit tes trois modèles dans le bon ordre : d'abord les deux staging, puis le mart qui en dépend. Tu n'as rien ordonné toi-même, il a lu les ref() et déduit le graphe. Va voir dans Snowsight, déplie ANALYTICS > DEV : tes trois vues sont là.

Tu peux supprimer le mon_premier_modele.sql de l'article précédent, il ne sert plus à rien. Garde ton dossier models/ propre dès le début.

Vérifier que tes sources sont à jour

Un pipeline qui tourne sur des données périmées, c'est pire qu'un pipeline en erreur, parce que personne ne s'en rend compte. dbt sait surveiller la fraîcheur de tes sources grâce à la colonne _loaded_at qu'on a prévue.

Complète ton _sources.yml :

version: 2

sources:
  - name: raw
    database: analytics
    schema: raw
    tables:
      - name: customers
      - name: orders
        config:
          loaded_at_field: _loaded_at
          freshness:
            warn_after: {count: 12, period: hour}
            error_after: {count: 24, period: hour}

Puis lance :

dbt source freshness

dbt regarde la valeur max de _loaded_at dans raw.orders. Si la donnée la plus récente a plus de 12 heures, il te met un warning. Plus de 24 heures, une erreur. En prod, tu branches cette commande sur ton orchestrateur et tu es prévenu dès qu'une source arrête d'être alimentée.

Le lien avec l'architecture Médaillon

Si tu as lu mon article sur le Médaillon, tu as déjà reconnu le schéma :

Couche Médaillon Dans dbt Ce qu'on y fait
Bronze les sources (raw) donnée brute, jamais modifiée
Silver le staging (stg_) nettoyage léger, renommage, cast
Gold les marts logique métier, agrégats, modèles finaux

Ton mart_clients_commandes est un premier pas vers la couche Gold. On le construira proprement plus tard, avec des dimensions et des faits. Pour l'instant retiens la logique : la donnée brute reste intacte, et chaque couche fait une seule chose.

Ce qu'on a fait, et la suite

Récap. de ce qu'on a vu dans cet article :

  • Créer une couche brute raw dans Snowflake (le Bronze)
  • Déclarer les sources dans _sources.yml et utilisé source()
  • Construire des modèles de staging stg_ avec un nettoyage léger (le Silver)
  • Chainer les modèles avec ref() et laissé dbt gérer l'ordre
  • Mettre en place un contrôle de fraîcheur avec dbt source freshness.

Tu as maintenant un vrai projet structuré en couches. Dans le prochain article, on s'attaque aux matérialisations et pourquoi les modèles sont des vues par défaut, quand les transformer en tables, et comment dbt gère tout ça avec un seul mot-clé.


Aller plus loin

Cet article fait partie de la formation dbt complète, du premier modèle au déploiement en production.

👉 Suivre toute la formation dbt

dbt tourne sur Snowflake dans ce parcours. Pour maîtriser le socle (warehouses, rôles, ingestion) :

👉 Accéder à la formation Snowflake

Et pour t'entraîner sur dbt en conditions d'examen, la certification dbt Analytics Engineering teste exactement ces concepts de sources, staging et ref().

👉 Préparer la certification dbt sur DataCertification.fr

Tu veux que je t'accompagne sur ton projet data (Snowflake, dbt, modélisation, industrialisation) ?

👉 Réserver un appel de 30 minutes


Questions fréquentes

Quelle différence entre source() et ref() en dbt ?

source() pointe vers une table brute externe, déclarée dans un fichier sources YAML, que dbt ne construit pas lui-même. ref() pointe vers un autre modèle dbt, c'est-à-dire une table ou une vue que dbt fabrique. La règle simple : source() pour entrer la donnée dans le projet, ref() pour relier les modèles entre eux.

Faut-il un modèle de staging pour chaque table source ?

Oui, c'est la bonne pratique. Un modèle de staging par table source, avec uniquement du nettoyage léger : renommage, cast de types, harmonisation. Toute la logique métier, les jointures et les agrégats viennent dans les couches suivantes. Ça garde chaque modèle simple et réutilisable.

C'est quoi la convention de nommage stg_ en dbt ?

Préfixer les modèles de staging par stg_ est une norme de la communauté dbt. Elle n'a aucun effet technique mais rend le projet lisible : on identifie immédiatement une couche d'entrée. Quand le projet grossit, on range aussi chaque système source dans son propre sous-dossier de staging.

Comment vérifier que mes données sources sont à jour ?

dbt propose un contrôle de fraîcheur. Tu déclares un champ horodaté avec loaded_at_field et des seuils warn_after et error_after dans ton fichier sources, puis tu lances dbt source freshness. dbt compare la valeur la plus récente de ce champ à l'heure actuelle et t'alerte si la source n'est plus alimentée.

Quel est le lien entre les sources dbt et l'architecture Médaillon ?

Les sources dbt correspondent à la couche Bronze (donnée brute non modifiée), les modèles de staging à la couche Silver (donnée nettoyée et normalisée), et les marts à la couche Gold (logique métier et modèles finaux). dbt est l'outil qui matérialise ce passage d'une couche à l'autre.