dbt: Configurations Hiérarchiques dbt : Gestion Prédictible des Schémas

🎯 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

ApprochePrévisibilitéMaintenanceFlexibilité
Config à plat❌ Imprévisible❌ Duplication⚠️ Limitée
Config par modèle❌ Dispersée❌ Répétitive✅ Maximale
🎯 Hiérarchique + OverrideParfaiteMinimaleContrô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 ! 🎯

Leave a Reply

Your email address will not be published. Required fields are marked *