La structure des dossiers d’un projet dbt est l’une des premières décisions à prendre au démarrage d’un projet, et l’une des plus difficiles à modifier par la suite. Les notes ci-dessous décrivent la disposition des dossiers, les conventions de nommage des modèles, les responsabilités par couche et les patterns de configuration dbt_project.yml.
Disposition des répertoires
Le répertoire models/ utilise trois dossiers de premier niveau qui reflètent l’architecture trois couches : base/, intermediate/ et marts/. Ce nommage est intentionnel — l’ordre alphabétique correspond à l’ordre de la lignée, de sorte qu’ouvrir le dossier montre le flux de données de gauche à droite dans le DAG.
my_project/├── dbt_project.yml├── packages.yml├── macros/│ ├── _macros.yml│ ├── generate_schema_name.sql│ └── marketing/│ ├── channel_grouping.sql│ └── attribution_weight.sql├── models/│ ├── base/│ │ ├── stripe/│ │ │ ├── _stripe__sources.yml│ │ │ ├── _stripe__models.yml│ │ │ ├── base__stripe__payment.sql│ │ │ └── base__stripe__customer.sql│ │ └── ga4/│ │ ├── _ga4__sources.yml│ │ ├── _ga4__models.yml│ │ └── base__ga4__event.sql│ ├── intermediate/│ │ ├── _int__models.yml│ │ ├── session/│ │ │ ├── int__session.sql│ │ │ └── int__session__session_lj_conversion.sql│ │ └── customer/│ │ └── int__customer__customer_lj_order.sql│ └── marts/│ ├── finance/│ │ ├── _finance__models.yml│ │ └── mrt__finance__order.sql│ └── marketing/│ ├── _marketing__models.yml│ ├── mrt__marketing__session.sql│ └── mrt__marketing__campaign_performance.sql├── seeds/│ ├── _seeds.yml│ └── channel_mapping.csv├── snapshots/│ └── snap__customer.sql└── tests/ └── assert_attribution_sums_to_one.sqlChaque couche utilise un principe d’organisation différent :
- Base est organisé par système source (
stripe/,ga4/,hubspot/). Nommez les dossiers d’après la source, pas d’après le chargeur — utilisezstripe/, pasfivetran/. Votre chargeur peut changer ; votre système source est plus stable. - Intermediate est organisé par entité (
session/,customer/). Ajoutez des sous-dossiers lorsque vous avez 3 modèles ou plus pour la même entité. N’organisez jamais par domaine métier ici —int__customer__customer_lj_ordersert à la fois la finance et le marketing. - Marts est organisé par domaine métier (
finance/,marketing/). C’est là que les consommateurs cherchent les données, et ils pensent en termes métier, pas en systèmes sources.
Limitez la profondeur des dossiers à trois niveaux maximum. Une imbrication profonde comme models/staging/external/stripe/payments/v2/ est un cauchemar de navigation.
La convention de nommage avec double underscore
Le double underscore (__) crée une séparation visuelle non ambiguë entre les composants d’un nom de modèle. Comparez :
base__google_analytics__campaign— clairement couche base, source google_analytics, entité campaignbase_google_analytics_campaign— est-ce google + analytics_campaign ? ou google_analytics + campaign ?
Avec des noms de source ou d’entité composés de plusieurs mots, l’ambiguïté se multiplie. Les doubles underscores l’éliminent.
Nommage par couche
| Couche | Pattern | Exemple |
|---|---|---|
| Base | base__[source]__[entité] | base__stripe__payment |
| Intermediate (pure) | int__[entité] | int__session |
| Intermediate (enrichie) | int__[entité]__[entité1]_[join]_[entité2] | int__customer__customer_lj_order |
| Marts | mrt__[département]__[entité] | mrt__marketing__campaign_performance |
| Snapshots | snap__[entité] | snap__customer |
Les modèles base ont une relation 1-pour-1 avec une table source. Le nom encode le système source et l’entité : base__ga4__event, base__stripe__customer.
Les modèles intermediate purs (int__session, int__customer) appliquent la logique métier à une seule entité — sessionisation, déduplication, calculs complexes. Ne les créez que lorsque vous ajoutez de la valeur au-delà du modèle base.
Les modèles intermediate enrichis encodent les informations de jointure directement dans le nom. int__customer__customer_lj_order indique : grain customer, LEFT JOIN vers order. Les abréviations de jointure sont lj (LEFT JOIN), ij (INNER JOIN), cj (CROSS JOIN). Pour plusieurs jointures, chaînez-les : int__customer__customer_lj_order_lj_session. Verbeux, mais entièrement auto-documenté — vous connaissez le grain, les entités jointes et les types de jointure sans ouvrir le SQL.
Les modèles mart incluent le domaine métier : mrt__marketing__session, mrt__finance__revenue. Le marketing n’a pas besoin de savoir que les données de session proviennent de GA4 ; il lui importe qu’il s’agisse de données marketing.
Noms d’entités au singulier
Utilisez des noms au singulier : customer, order, session, campaign. Chaque ligne représente une instance de l’entité. Cela garantit également la cohérence du nommage entre les couches : base__stripe__customer, int__customer et mrt__finance__customer font tous référence à la même entité.
Organisation YAML
Utilisez le pattern par répertoire : un fichier YAML par dossier, préfixé par un underscore pour qu’il apparaisse en tête de liste.
base/stripe/├── _stripe__sources.yml # Définitions des sources├── _stripe__models.yml # Configs des modèles, tests, docs├── base__stripe__payment.sql└── base__stripe__customer.sqlLe préfixe underscore fait apparaître les fichiers YAML avant les fichiers SQL. Inclure le nom du répertoire (_stripe__models plutôt que _models) accélère la recherche floue dans les éditeurs.
Gardez les définitions de sources et les définitions de modèles dans des fichiers séparés. _stripe__sources.yml contient les blocs sources:, les tests de fraîcheur et la documentation des sources. _stripe__models.yml contient les configurations des modèles, les tests de colonnes et les descriptions. Les mélanger crée de la confusion.
N’utilisez jamais un schema.yml monolithique à la racine du projet. Un fichier YAML de 2000 lignes est impossible à rechercher, à maintenir, et génère constamment des conflits de fusion.
Configuration dbt_project.yml
Le fichier projet contrôle les valeurs par défaut. Définissez la matérialisation en table globalement, puis surchargez selon les besoins :
name: my_projectversion: '1.0.0'
vars: session_timeout_minutes: 30
models: my_project: +materialized: table base: +schema: base ga4: +materialized: incremental +incremental_strategy: insert_overwrite intermediate: +schema: intermediate marts: +schema: marts marketing: +group: marketing +access: publicCette configuration fait plusieurs choses. La propriété +schema pousse chaque couche dans son propre schéma (base, intermediate, marts), ce qui clarifie dans l’entrepôt à quelle couche appartient une table. Les sources à fort volume comme GA4 surchargent la valeur par défaut pour utiliser la matérialisation incrémentielle. Les groupes et modificateurs d’accès documentent la propriété et font respecter les frontières entre domaines.
Les tables partout est la valeur par défaut recommandée. Le stockage est bon marché ; la visibilité pour le débogage ne l’est pas. Les vues se recalculent à chaque requête et propagent instantanément les ruptures de schéma. Les modèles éphémères sont invisibles dans l’entrepôt, rendant le débogage impossible. Réservez incremental pour les tables dépassant plusieurs millions de lignes, et view pour les rares cas où les données doivent être fraîches à la minute.
Macros, seeds, snapshots et tests
Les macros se regroupent par domaine ou fonction. Les macros de surcharge (generate_schema_name.sql) vivent à la racine de macros/. Les macros utilitaires vont dans utils/. Les macros spécifiques à un domaine vont dans des sous-dossiers (marketing/, finance/). Une macro par fichier, le nom de fichier correspondant au nom de la macro — quand vous avez besoin de channel_grouping, vous savez qu’elle se trouve dans channel_grouping.sql. Documentez chaque macro dans _macros.yml avec son objectif, ses arguments et un exemple d’utilisation.
Les seeds sont des fichiers CSV pour les tables de référence statiques qui n’existent dans aucun système source : mappings UTM-vers-canal, codes pays, adresses IP internes à exclure. N’utilisez pas les seeds pour charger des données réelles ou des jeux de données volumineux.
Les snapshots créent des enregistrements de dimension à variation lente de type 2. Nommez-les snap__[entité]. Depuis dbt 1.9+, vous pouvez définir les snapshots en YAML plutôt qu’en SQL.
Les tests se divisent en tests génériques (déclarés en YAML aux côtés des modèles) et tests singuliers (fichiers SQL dans tests/). Au minimum, testez chaque clé primaire pour unique et not_null. Pour un aperçu complet de tous les types de tests — tests unitaires, contrats, dbt-expectations — consultez la taxonomie des tests dbt.
Faire respecter les conventions
Les conventions ne fonctionnent que si elles sont appliquées. Le package dbt-project-evaluator audite automatiquement votre projet par rapport aux bonnes pratiques : tests de clé primaire manquants, modèles sans descriptions, références directes aux sources dans les marts, violations de nommage. Ajoutez-le à packages.yml et exécutez-le en CI. Vous pouvez le configurer pour correspondre à vos conventions (par exemple, les préfixes base__ au lieu du stg_ par défaut).
Pour les équipes utilisant Claude Code, documenter vos conventions de nommage dans CLAUDE.md à la racine du projet donne à l’IA une mémoire persistante de votre structure. Cela l’empêche de générer des préfixes stg_ quand votre projet utilise base__, ou de créer des modèles dans le mauvais répertoire.
Erreurs structurelles courantes
Organiser la couche base par chargeur plutôt que par source. fivetran/ et airbyte/ sont des détails d’implémentation. stripe/ et ga4/ sont les origines réelles des données.
Logique métier dans les modèles base. Si vous voyez des instructions CASE avec des règles métier dans un modèle base, cette logique appartient à la couche intermediate. La base doit être mécanique : renommer, caster, filtrer, dédupliquer.
Sauter la couche intermediate. Des marts avec 10+ JOINs et une logique dupliquée sur plusieurs modèles sont le signe que vous devez centraliser les jointures et transformations partagées en intermediate.
Imbrication excessive des dossiers. Si vous devez cliquer dans cinq répertoires pour trouver un modèle, votre structure travaille contre vous.
Tout en éphémère. Vous ne pouvez pas faire SELECT * FROM int__session LIMIT 100 si c’est éphémère. Les tables sont bon marché. La visibilité pour le débogage est inestimable.
YAML monolithique. Un fichier par répertoire. Toujours.