Le pattern à trois couches — base, intermediate, marts — organise un projet dbt de sorte que chaque couche ait un rôle défini avec des frontières claires. Chaque couche a une fonction spécifique. Des frontières floues entraînent de la logique métier dans les modèles base, des agrégations dans les couches intermediate, et des marts qui se dupliquent les uns les autres.
Les trois couches en un coup d’œil
Base : Nettoyage des données brutes. Renommer les colonnes, caster les types, gérer les particularités des sources. Aucune logique métier, aucune jointure.
Intermediate : Combiner et enrichir. Joindre les modèles base, ajouter des calculs, appliquer les règles métier. Préserver chaque ligne — ne jamais agréger ici.
Marts : Agréger pour la consommation. Construire des tables pour des usages spécifiques : dashboards, reverse ETL, modèles ML. C’est ici que le GROUP BY a sa place.
Pensez-y comme un pipeline : données sources → fondation nettoyée → entités enrichies → tables prêtes à l’emploi.
Couche base : le fondement
Les modèles base rendent les données sources brutes utilisables sans les interpréter.
Ce qui appartient aux modèles base :
- Renommer les colonnes selon des conventions de nommage cohérentes
- Caster les types de données (timestamps string vers timestamps réels, entiers vers booléens)
- Gérer les particularités propres à la source : dénester les champs imbriqués, dédupliquer sur la clé primaire
- Filtrer les données indésirables (enregistrements de test, lignes soft-deleted)
- Conversions d’unités (millisecondes vers secondes, centimes vers euros)
Ce qui n’y appartient pas :
- Les jointures (avec une exception étroite : si votre source a scindé ce qui est logiquement une seule table en plusieurs — comme orders et order_metadata — corriger cela en base est acceptable)
- La logique métier ou les calculs
- Toute forme d’agrégation
Les modèles base doivent être purement mécaniques : quelqu’un qui ne connaît pas le domaine métier doit pouvoir lire le SQL et comprendre rapidement la transformation.
Un modèle base typique pour une source e-commerce pourrait renommer user_id en customer__id, caster created_at en timestamp correct, dédupliquer sur l’ID de commande et filtrer les commandes de test. Il ne calcule pas les marges, ne catégorise pas les commandes, ni ne signale les clients à forte valeur. Ce sont des préoccupations pour l’intermediate.
Pourquoi c’est important
Les modèles base font office de contrat entre votre projet dbt et les données brutes. Lorsqu’un schéma source change, seuls les modèles base se cassent — pas tout ce qui est en aval. Lorsque vous devez déboguer un calcul, remonter jusqu’à la base est simple car la base est fiable.
Couche intermediate
Les modèles intermediate joignent des entités, encodent la logique métier et ajoutent des champs calculés.
La contrainte critique : vous ne réduisez jamais la granularité. Si vous construisez un modèle de commandes enrichies, chaque ligne reste une commande. Si vous construisez un modèle de sessions enrichies, chaque ligne reste un événement ou une session. Vous ajoutez des colonnes, jamais moins de lignes.
Ce qui appartient ici :
- Joindre les modèles base pour créer des entités enrichies
- Logique de sessionisation (regroupement d’événements en unités cohérentes)
- Calculs métier (taux de marge, segments clients, flags de récence)
- Fonctions de fenêtre (numéros de séquence, totaux cumulés, classements)
- Logique de déduplication complexe (fusion d’enregistrements clients depuis le CRM et la facturation)
Ce qui n’y appartient pas :
- Les agrégations qui réduisent le nombre de lignes en sortie finale (avec une exception : les tables de correspondance construites temporairement pour enrichir jusqu’à la granularité d’origine)
Considérons un exemple avec les commandes : vous joignez base__orders avec base__customers, ajoutez un champ calculé margin_usd, attribuez à chaque commande un customer__order_number via des fonctions de fenêtre, et calculez order__is_first_order. Le résultat a le même nombre de lignes que le modèle base des commandes — il est juste plus large et plus riche.
Pour les modèles intermediate fortement axés sur les agrégations, l’exception s’applique. Imaginez que vous ayez besoin de métriques au niveau client (nombre de commandes, total dépensé) pour enrichir un modèle à granularité commande. Vous construiriez une CTE qui agrège les commandes au niveau client, puis la jointure via LEFT JOIN sur la table des commandes. La sortie finale est toujours une ligne par commande ; l’agrégation n’était qu’une étape intermédiaire.
Organisation centrée sur les entités
Les modèles intermediate doivent représenter des entités métier propres et bien définies à une granularité spécifique, pas des transformations. C’est une différence cruciale. Un modèle intermediate nommé int__orders__enriched est préférable à int__orders__with_customer_data_pivoted. Le nom doit indiquer quelle entité et quelle granularité vous obtenez, pas les acrobaties SQL utilisées pour le construire.
Lorsque vous avez plusieurs modèles intermediate joignant différentes entités, les noms auto-documentés aident. int__customer__customer_lj_order indique clairement : c’est un modèle à granularité client avec un LEFT JOIN sur les commandes. Le nom vous donne la granularité et le pattern de jointure sans ouvrir le fichier SQL.
Couche mart : construite pour la consommation
Les marts sont le dernier arrêt. Contrairement à la base (organisée par système source) et à l’intermediate (organisée par entité), les marts sont organisés par cas d’usage et par consommateur. Un mart agrège jusqu’à la granularité dont une équipe spécifique a besoin : performance quotidienne par canal, une ligne par client avec tous les attributs pour une synchronisation CRM, colonnes de features pour les modèles ML.
Ce qui appartient aux marts :
- Agrégations vers une granularité de reporting spécifique
- Métriques métier finales et taux calculés (taux de conversion, revenu par session)
- Formatage et nommage des colonnes spécifiques au consommateur
- Tables larges dénormalisées prêtes pour les dashboards
La différence clé : les marts sont construits pour quelqu’un. Pas à usage général. Chaque mart répond à des questions spécifiques à des niveaux d’agrégation spécifiques. Un mart peut agréger quotidiennement × par canal. Un autre peut agréger annuellement par région. Un autre peut être une ligne par client pour le reverse ETL.
Un exemple de mart de reporting : vous agrégez des événements GA4 sessionisés jusqu’à une granularité quotidienne × source × medium, en calculant le nombre de sessions journalières, d’utilisateurs, les conversions et le revenu par session. C’est prêt pour le dashboard.
Un mart d’activation (pour la synchronisation CRM) est complètement différent : une ligne par client avec tous les attributs attendus par le système de destination, formatés pour correspondre exactement à son schéma.
L’agrégation se fait ici car c’est là que vous réduisez les données à ce dont les consommateurs ont réellement besoin.
Flux de données et pourquoi ça compte
Les données circulent source → base → intermediate → marts.
Cette lignée a des implications :
-
Les modèles base sont stables. Ils sont proches de la vérité source. Les marts en dépendent indirectement, mais une rupture dans la base ne casse pas immédiatement tous les marts car l’intermediate sert de tampon.
-
Les modèles intermediate sont réutilisables. Plusieurs marts peuvent référencer le même intermediate. Si vous avez besoin de métriques client dans un mart de dashboard et un mart d’activation, les deux référencent
int__customer__enriched. Modifiez-le une fois, les deux marts le voient. C’est pourquoi l’intermediate existe. -
Les marts sont spécialisés. Parce qu’ils sont spécifiques à un cas d’usage, ils peuvent être agressifs sur le formatage, l’agrégation et la sélection des colonnes. Ils ne cherchent pas à servir tout le monde ; ils cherchent à résoudre un problème précis.
Cette structure passe à l’échelle. Un petit projet peut sauter l’intermediate entièrement (base → marts). Un grand projet peut avoir 30 modèles base (un par source), 10 modèles intermediate (un par entité), et 20 marts (consommateurs variés).
Erreurs courantes
Agrégations dans l’intermediate : Construire int__daily_sales qui groupe par date. Stop. Si ça réduit les lignes, c’est un mart. L’intermediate doit préserver la granularité.
Logique métier dans la base : Calculer des segments clients ou des niveaux de valeur dans un modèle staging. Déplacez-le vers l’intermediate. La base doit être mécanique.
Marts qui n’agrègent pas : SELECT * FROM int__order__enriched renommé et appelé mart. Si vous ne changez pas la granularité ou ne formatez pas pour un consommateur spécifique, vous n’avez probablement pas besoin d’un mart.
Propriété floue : Deux modèles intermediate qui calculent chacun la valeur à vie différemment. Choisissez-en un. Faites-en la source de vérité. Tout le reste le référence.
Sauter l’intermediate quand il le faut : Des marts avec 10 jointures et une logique répétée dans plusieurs modèles. Cela signale que vous avez besoin de couches intermediate pour centraliser les jointures et la logique.
Quand ajouter ou sauter des couches
Tous les projets n’ont pas besoin des trois couches.
Projets simples (< 20 modèles) : Base + marts peut suffire. Si vos marts sont des agrégations directes de modèles base uniques, l’intermediate ajoute une surcharge.
Projets en croissance : Une fois que vous joignez 3+ modèles base à plusieurs endroits ou que vous dupliquez la logique de jointure entre les marts, extrayez cette logique vers l’intermediate. Une fois que les temps de build des marts ralentissent, envisagez des modèles intermediate incrémentaux.
Grands projets (200+ modèles) : Les trois couches deviennent essentielles. Vous aurez probablement des sous-dossiers organisés : base/stripe/, intermediate/customer/, marts/finance/.
Le cadre de décision : si vous copiez-collez de la logique de jointure entre les marts, vous avez besoin d’un modèle intermediate.
Clarté de nommage
Utilisez des préfixes pour rendre les couches évidentes : base__, int__, mrt__. Incluez la source pour la base (base__stripe__payment), l’entité pour l’intermediate (int__session, int__customer__customer_lj_order), et le domaine pour les marts (mrt__marketing__campaign_performance).
Le pattern enrichi intermediate int__[primaire]__[entité]_[jointure]_[entité] est verbeux mais auto-documenté. int__customer__customer_lj_order vous indique la granularité, ce qui est joint et comment.
Pour des exemples d’implémentation détaillés avec le code SQL complet, voir Patterns de la couche base dans dbt, Patterns de la couche intermediate dans dbt, et Patterns de la couche mart dans dbt. Pour les conseils sur la structure du projet, voir Structure et nommage d’un projet dbt.