🎯 Le Problème avec les Sous-répertoires Imbriqués
Structure de Projet Typique
models/
├── staging/
│ ├── ecommerce/
│ │ ├── orders/
│ │ │ ├── stg_orders.sql
│ │ │ └── stg_order_items.sql
│ │ └── customers/
│ │ ├── stg_customers.sql
│ │ └── stg_customer_profiles.sql
│ └── marketing/
│ ├── campaigns/
│ │ └── stg_campaigns.sql
│ └── events/
│ └── stg_web_events.sql
├── intermediate/
│ ├── finance/
│ │ └── int_revenue_calculations.sql
│ └── customer_analytics/
│ └── int_customer_metrics.sql
└── marts/
├── finance/
│ ├── executive/
│ │ └── executive_dashboard.sql
│ └── operational/
│ └── daily_revenue.sql
└── marketing/
└── customer_segmentation.sql
🚨 Défis Sans Configuration Hiérarchique
1. Répétition de configuration :
# ❌ Configuration répétée partout
# staging/ecommerce/orders/schema.yml
models:
- name: stg_orders
config:
schema: staging_ecommerce # Répété...
# staging/ecommerce/customers/schema.yml
models:
- name: stg_customers
config:
schema: staging_ecommerce # Répété...
# staging/marketing/campaigns/schema.yml
models:
- name: stg_campaigns
config:
schema: staging_marketing # Différent mais répété...
2. Confusion sur l’origine des configurations :
-- ❌ D'où vient le schéma de ce modèle ?
-- Défini dans le modèle ? Dans dbt_project.yml ? Dans schema.yml ?
SELECT * FROM customers
3. Maintenance difficile :
# ❌ Si vous voulez changer "staging_ecommerce" en "staging_commerce"
# Il faut modifier 15+ fichiers différents !
✅ Solution : Configurations Hiérarchiques avec Overrides Explicites
Principe Clé
Définir les configurations par niveaux avec une hiérarchie claire et des overrides explicites quand nécessaire.
🛠️ Implémentation Complète
Niveau 1 : Configuration Globale (dbt_project.yml)
# dbt_project.yml - Configuration hiérarchique
name: 'ecommerce_analytics'
version: '1.0.0'
models:
ecommerce_analytics:
# 🔹 NIVEAU RACINE : Configurations par défaut
+materialized: view
+docs:
show: true
# 🔸 NIVEAU 1 : Grandes catégories
staging:
+schema: staging # Schéma par défaut pour tout staging
+materialized: view
+tags: ['staging', 'source_data']
# 🔹 NIVEAU 2 : Domaines métier
ecommerce:
+schema: staging_ecommerce # Override : schéma spécialisé
+tags: ['staging', 'ecommerce']
# 🔸 NIVEAU 3 : Sous-domaines
orders:
+schema: staging_orders # Override : encore plus spécialisé
+materialized: table # Override : matérialisation différente
+tags: ['staging', 'ecommerce', 'orders', 'high_volume']
customers:
+schema: staging_customers # Override : schéma dédié customers
+post_hook: "ANALYZE TABLE {{ this }}" # Hook spécifique
marketing:
+schema: staging_marketing # Override : domaine marketing
+tags: ['staging', 'marketing']
campaigns:
+schema: staging_campaigns # Override : sous-domaine campaigns
+materialized: incremental # Override : incremental pour performance
intermediate:
+schema: intermediate # Schéma par défaut
+materialized: ephemeral # Materialization par défaut
+tags: ['intermediate']
finance:
+schema: intermediate_finance # Override : finance séparé
+materialized: table # Override : table pour finance
+tags: ['intermediate', 'finance', 'sensitive']
customer_analytics:
+schema: analytics_customers # Override : schéma business-friendly
+materialized: table
marts:
+schema: marts # Schéma par défaut
+materialized: table # Table par défaut pour marts
+tags: ['marts', 'production']
finance:
+schema: finance # Override : schéma business direct
+tags: ['marts', 'finance', 'executive']
+post_hook: [
"GRANT SELECT ON {{ this }} TO ROLE finance_analysts",
"ANALYZE TABLE {{ this }}"
]
executive:
+schema: executive # Override : schéma VIP pour executives
+materialized: table
+pre_hook: "SET work_mem = '2GB'" # Performance boost
+tags: ['marts', 'finance', 'executive', 'critical']
operational:
+schema: finance_ops # Override : schéma opérationnel
+materialized: incremental # Override : incremental pour données fréquentes
+tags: ['marts', 'finance', 'operational', 'hourly_refresh']
marketing:
+schema: marketing # Override : schéma marketing
+tags: ['marts', 'marketing']
# Variables pour les overrides dynamiques
vars:
schema_overrides:
enable_custom_schemas: true
environment_suffix: "{{ '_' + target.name if target.name != 'prod' else '' }}"
Niveau 2 : Configuration au Niveau Dossier (schema.yml)
# models/staging/ecommerce/orders/schema.yml
version: 2
# ✅ Configuration explicite avec héritage visible
models:
- name: stg_orders
description: |
**Configuration héritée :**
- Schéma : staging_orders (défini dans dbt_project.yml niveau 3)
- Matérialisation : table (override niveau 3)
- Tags : staging, ecommerce, orders, high_volume
**Aucun override nécessaire** - configuration parfaite héritée
columns:
- name: order_id
description: "Identifiant unique de commande"
- name: stg_order_items
description: "Articles de commande"
# ✅ Override explicite si nécessaire
config:
schema: staging_order_details # Override : schéma encore plus spécifique
tags: ['staging', 'ecommerce', 'orders', 'items', 'high_volume']
columns:
- name: order_item_id
description: "Identifiant unique d'article"
# models/marts/finance/executive/schema.yml
version: 2
models:
- name: executive_dashboard
description: |
**Configuration héritée :**
- Schéma : executive (niveau 4 dans dbt_project.yml)
- Matérialisation : table (niveau 4)
- Pre-hook : SET work_mem = '2GB' (niveau 4)
- Tags : marts, finance, executive, critical
**Aucun override** - configuration executive parfaite
config:
# ✅ Override explicite seulement si vraiment nécessaire
post_hook: [
"GRANT SELECT ON {{ this }} TO ROLE c_suite", # Permission spéciale CEO
"{{ log('Executive dashboard updated: ' ~ run_started_at, info=True) }}"
]
columns:
- name: metric_date
description: "Date des métriques exécutives"
Niveau 3 : Configuration au Niveau Modèle (Override Explicite)
-- models/staging/ecommerce/customers/stg_customer_special.sql
-- ✅ Override explicite documenté dans le modèle
{{ config(
schema='staging_vip_customers', -- Override : schéma très spécifique
materialized='incremental', -- Override : incremental au lieu de view
unique_key='customer_id',
tags=['staging', 'ecommerce', 'customers', 'vip', 'special_handling'],
-- Documentation de l'override
meta={
'config_override_reason': 'VIP customers need separate schema for security',
'inherited_from': 'staging.ecommerce.customers in dbt_project.yml',
'overridden_properties': ['schema', 'materialized', 'tags']
}
) }}
/*
CONFIGURATION FINALE RÉSULTANTE :
- Schema: staging_vip_customers (OVERRIDE explicite)
- Materialization: incremental (OVERRIDE explicite)
- Tags: staging, ecommerce, customers, vip, special_handling (OVERRIDE + héritage)
- Post-hook: ANALYZE TABLE {{ this }} (HÉRITÉ de staging.ecommerce.customers)
*/
SELECT
customer_id,
customer_name,
vip_tier,
special_handling_required
FROM {{ source('raw_crm', 'vip_customers') }}
{% if is_incremental() %}
WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }})
{% endif %}
📊 Exemple Concret : Traçabilité des Configurations
Macro de Documentation Automatique
-- macros/config_lineage.sql
{% macro document_config_inheritance(model_name) %}
{% set model_node = graph.nodes.get('model.' ~ project_name ~ '.' ~ model_name) %}
{% if not model_node %}
{{ return("Model not found: " ~ model_name) }}
{% endif %}
{% set config_info = {
'schema': model_node.config.get('schema'),
'materialized': model_node.config.get('materialized'),
'tags': model_node.config.get('tags', []),
'pre_hook': model_node.config.get('pre_hook', []),
'post_hook': model_node.config.get('post_hook', [])
} %}
{{ log("📋 CONFIGURATION INHERITANCE pour " ~ model_name ~ ":", info=True) }}
{{ log(" 📁 Chemin: " ~ model_node.original_file_path, info=True) }}
{{ log(" 🗂️ Schéma final: " ~ config_info.schema, info=True) }}
{{ log(" 🏗️ Matérialisation: " ~ config_info.materialized, info=True) }}
{{ log(" 🏷️ Tags: " ~ (config_info.tags | join(', ')), info=True) }}
{% if model_node.config.get('meta', {}).get('config_override_reason') %}
{{ log(" ⚠️ Override explicite: " ~ model_node.config.meta.config_override_reason, info=True) }}
{{ log(" 📜 Propriétés overridden: " ~ (model_node.config.meta.overridden_properties | join(', ')), info=True) }}
{% endif %}
{% endmacro %}
-- Usage dans un modèle
-- models/staging/ecommerce/orders/stg_orders.sql
{{ config(
pre_hook="{{ document_config_inheritance('stg_orders') }}"
) }}
SELECT * FROM {{ source('ecommerce', 'orders') }}
Sortie de Documentation Automatique
📋 CONFIGURATION INHERITANCE pour stg_orders:
📁 Chemin: models/staging/ecommerce/orders/stg_orders.sql
🗂️ Schéma final: staging_orders
🏗️ Matérialisation: table
🏷️ Tags: staging, ecommerce, orders, high_volume
📈 Héritage: staging.ecommerce.orders (niveau 3 dans dbt_project.yml)
✅ Aucun override nécessaire - configuration héritée parfaite
🔍 Validation et Tests de Configuration
Test de Cohérence Hiérarchique
-- tests/test_schema_inheritance.sql
WITH expected_schemas AS (
SELECT
'stg_orders' as model_name,
'staging_orders' as expected_schema,
'Niveau 3: staging.ecommerce.orders' as inheritance_source
UNION ALL
SELECT
'stg_customers' as model_name,
'staging_customers' as expected_schema,
'Niveau 3: staging.ecommerce.customers' as inheritance_source
UNION ALL
SELECT
'executive_dashboard' as model_name,
'executive' as expected_schema,
'Niveau 4: marts.finance.executive' as inheritance_source
),
actual_schemas AS (
SELECT
table_name as model_name,
table_schema as actual_schema
FROM information_schema.tables
WHERE table_schema LIKE '{{ target.schema }}%'
)
SELECT
e.model_name,
e.expected_schema,
a.actual_schema,
e.inheritance_source,
CASE
WHEN e.expected_schema = REPLACE(a.actual_schema, '{{ target.schema }}_', '')
THEN '✅ Configuration correcte'
ELSE '❌ Configuration incorrecte'
END as schema_validation
FROM expected_schemas e
LEFT JOIN actual_schemas a ON e.model_name = a.model_name
WHERE a.model_name IS NULL
OR e.expected_schema != REPLACE(a.actual_schema, '{{ target.schema }}_', '')
Dashboard de Configuration
-- models/governance/configuration_hierarchy_audit.sql
{{ config(materialized='table') }}
WITH model_configurations AS (
SELECT
'stg_orders' as model_name,
'models/staging/ecommerce/orders/' as model_path,
'staging_orders' as final_schema,
'table' as final_materialization,
'staging.ecommerce.orders (niveau 3)' as inheritance_source,
ARRAY['staging', 'ecommerce', 'orders', 'high_volume'] as final_tags,
FALSE as has_explicit_override
UNION ALL
SELECT
'stg_customer_special' as model_name,
'models/staging/ecommerce/customers/' as model_path,
'staging_vip_customers' as final_schema,
'incremental' as final_materialization,
'OVERRIDE: Model-level config' as inheritance_source,
ARRAY['staging', 'ecommerce', 'customers', 'vip', 'special_handling'] as final_tags,
TRUE as has_explicit_override
UNION ALL
SELECT
'executive_dashboard' as model_name,
'models/marts/finance/executive/' as model_path,
'executive' as final_schema,
'table' as final_materialization,
'marts.finance.executive (niveau 4)' as inheritance_source,
ARRAY['marts', 'finance', 'executive', 'critical'] as final_tags,
FALSE as has_explicit_override
),
configuration_summary AS (
SELECT
model_name,
model_path,
final_schema,
final_materialization,
inheritance_source,
ARRAY_TO_STRING(final_tags, ', ') as tags_list,
has_explicit_override,
-- Analyse des patterns
CASE
WHEN has_explicit_override THEN '⚠️ Override Explicite'
WHEN inheritance_source LIKE '%niveau 4%' THEN '🎯 Configuration Précise'
WHEN inheritance_source LIKE '%niveau 3%' THEN '🎛️ Configuration Spécialisée'
WHEN inheritance_source LIKE '%niveau 2%' THEN '📁 Configuration Domaine'
ELSE '🔧 Configuration Par Défaut'
END as config_pattern,
-- Score de complexité
CASE
WHEN has_explicit_override THEN 'High'
WHEN inheritance_source LIKE '%niveau 4%' THEN 'Medium-High'
WHEN inheritance_source LIKE '%niveau 3%' THEN 'Medium'
ELSE 'Low'
END as complexity_score
FROM model_configurations
)
SELECT
model_name,
model_path,
final_schema,
final_materialization,
tags_list,
inheritance_source,
config_pattern,
complexity_score,
has_explicit_override,
-- Recommandations
CASE
WHEN has_explicit_override AND complexity_score = 'High'
THEN '📝 Documenter la raison de l\'override'
WHEN NOT has_explicit_override AND complexity_score = 'Low'
THEN '✅ Configuration optimale'
ELSE '🔍 Révision recommandée'
END as maintenance_recommendation,
CURRENT_TIMESTAMP as audit_date
FROM configuration_summary
ORDER BY
CASE complexity_score
WHEN 'High' THEN 1
WHEN 'Medium-High' THEN 2
WHEN 'Medium' THEN 3
ELSE 4
END,
model_name
📈 Avantages de cette Approche
✅ Prévisibilité
# Vous savez EXACTEMENT d'où vient chaque configuration
models/staging/ecommerce/orders/stg_orders.sql
└── Hérite de: staging.ecommerce.orders (niveau 3)
└── Schema: staging_orders
└── Materialization: table
└── Tags: staging, ecommerce, orders, high_volume
✅ Réduction de la Duplication
# ❌ Avant : 20 modèles × 5 lignes de config = 100 lignes
# ✅ Après : 1 configuration hiérarchique = 5 lignes pour tout
# Une seule définition pour 20 modèles !
staging:
ecommerce:
orders:
+schema: staging_orders
+materialized: table
✅ Overrides Explicites et Documentés
{{ config(
schema='staging_vip_customers', -- Override explicite
meta={
'config_override_reason': 'VIP customers need separate schema for security'
}
) }}
✅ Maintenance Simplifiée
# Changer le schéma de tous les modèles ecommerce/orders ?
# Modification de 1 seule ligne dans dbt_project.yml !
staging:
ecommerce:
orders:
+schema: staging_order_management # Une ligne = 20 modèles mis à jour
🎯 Règles d’Or pour l’Implémentation
1. Principe de Moindre Surprise
# ✅ Configuration intuitive qui suit l'arborescence
models/
├── staging/ # → schema: staging
│ └── ecommerce/ # → schema: staging_ecommerce
│ └── orders/ # → schema: staging_orders
2. Override Documenté
-- ✅ Toujours documenter WHY vous overridez
{{ config(
schema='custom_schema',
meta={'override_reason': 'Explication claire du pourquoi'}
) }}
3. Hiérarchie Logique
# ✅ Niveaux qui reflètent l'organisation business
models:
project:
staging: # Niveau 1: Phase de données
ecommerce: # Niveau 2: Domaine métier
orders: # Niveau 3: Sous-domaine
4. Testing de Cohérence
-- ✅ Tests automatiques pour valider la configuration
-- Ensures expected schemas match actual schemas
💡 Comparaison des Approches
Approche | Prévisibilité | Maintenance | Flexibilité |
---|---|---|---|
Config à plat | ❌ Imprévisible | ❌ Duplication | ⚠️ Limitée |
Config par modèle | ❌ Dispersée | ❌ Répétitive | ✅ Maximale |
🎯 Hiérarchique + Override | ✅ Parfaite | ✅ Minimale | ✅ Contrôlée |
🚀 Résultats Concrets
Avant (Configuration Dispersée)
# 50 fichiers schema.yml avec configs dupliquées
# Impossible de savoir d'où vient une config
# Maintenance = cauchemar
Après (Hiérarchique + Override)
# 1 dbt_project.yml avec hiérarchie claire
# Override explicites documentés
# Configuration prévisible à 100%
# Maintenance = 1 ligne à modifier
Cette approche transforme la gestion des configurations d’un “jeu de devinettes” en un “système GPS” qui vous montre exactement d’où vient chaque configuration et comment la modifier ! 🎯