ARCHITECTURE & CULTURE
Patterns Architecturaux Cost-Aware
La majorité des coûts data se décide au moment du design, pas après. Un pipeline full-refresh sur une table de 500 GB coûtera 10 à 100x plus cher qu'un pipeline incrémental bien conçu.
Une architecture medallion mal calibrée peut doubler le storage sans bénéfice réel. Les patterns décrits ici sont ceux que j'applique systématiquement sur les missions d'optimisation. Ils ont un impact structurel et durable sur la facture.
Principe fondamental : Process Once, Query Many
C'est la règle numéro un de l'architecture cost-aware. Chaque donnée ne devrait être transformée qu'une seule fois, puis stockée dans un format optimisé pour la lecture. Toutes les requêtes analytiques consomment ensuite cette donnée pré-traitée.
-- ANTI-PATTERN : Chaque analyst refait le même calcul
-- Analyst A (coûte 2 GB à chaque exécution)
SELECT customer_id, SUM(amount) as total
FROM raw_transactions
WHERE date >= '2025-01-01'
GROUP BY customer_id;
-- Analyst B (refait le même scan, 2 GB de plus)
SELECT customer_id, SUM(amount) as total
FROM raw_transactions
WHERE date >= '2025-01-01'
GROUP BY customer_id;
-- PATTERN : Matérialiser une fois, lire N fois
-- Pipeline (exécuté 1x/jour, coûte 2 GB)
CREATE OR REPLACE TABLE gold.customer_totals AS
SELECT customer_id, SUM(amount) as total, MAX(date) as last_date
FROM raw_transactions
GROUP BY customer_id;
-- Analyst A (coûte 0.01 GB - table pré-agrégée)
SELECT * FROM gold.customer_totals WHERE total > 1000;
-- Analyst B (coûte 0.01 GB - même table)
SELECT * FROM gold.customer_totals;Succes
Décision concrète
Medallion Architecture et coûts
L'architecture medallion (Bronze → Silver → Gold) est le standard de facto pour les plateformes data modernes. Mais chaque couche a un profil de coût différent, et mal calibrer les couches revient à payer pour du storage et du compute inutiles.
┌──────────────────────────────────────────────────────────────┐
│ MEDALLION ARCHITECTURE │
│ Profil de coût par couche │
├──────────────────────────────────────────────────────────────┤
│ │
│ BRONZE (Raw) SILVER (Clean) GOLD (Business) │
│ ───────────── ────────────── ─────────────── │
│ Storage: $$$ Storage: $$ Storage: $ │
│ Write: $ Write: $$ Write: $$$ │
│ Read: $$$ Read: $$ Read: $ │
│ │
│ • Append-only • Dédupliqué • Pré-agrégé │
│ • Partitionné • Typé et validé • Matérialisé │
│ • Pas d'index • Clusteré • Cache-friendly│
│ • TTL agressif • Incrémental • Requêtes fast │
│ │
└──────────────────────────────────────────────────────────────┘Optimiser chaque couche
| Couche | Levier principal | Action concrète |
|---|---|---|
| Bronze | Réduire le storage | TTL 90 jours, compression, partitionnement par date |
| Silver | Réduire le compute d'écriture | Incremental processing, merge au lieu de full-refresh |
| Gold | Réduire le compute de lecture | Pré-agrégation, clustering aligné sur les filtres |
Erreur fréquente
À retenir
Incremental-first : le pattern le plus impactant
Le full-refresh est le pattern par défaut dans la plupart des projets dbt. C'est aussi le plus coûteux. Chaque exécution rescanne et réécrit l'intégralité de la table, même si seulement 0.1% des données ont changé.
-- dbt : full-refresh (défaut)
-- Rescanne et réécrit 100% de la table à chaque run
{{ config(materialized='table') }}
SELECT * FROM {{ ref('stg_events') }}
-- dbt : incremental
-- Ne traite que les nouvelles données depuis le dernier run
{{ config(
materialized='incremental',
incremental_strategy='merge',
unique_key='event_id',
partition_by={
"field": "event_date",
"data_type": "date",
"granularity": "day"
}
) }}
SELECT *
FROM {{ ref('stg_events') }}
{% if is_incremental() %}
WHERE event_date > (SELECT MAX(event_date) FROM {{ this }})
{% endif %}| Métrique | Full-refresh | Incremental |
|---|---|---|
| Données scannées/run | 500 GB | 1-5 GB |
| Coût par run (on-demand) | ~$2.50 | ~$0.025 |
| Coût mensuel (1 run/jour) | ~$75 | ~$0.75 |
| Durée d'exécution | 10-30 min | 30s-2 min |
À retenir
Erreur fréquente
Tiering du compute
Tous les workloads n'ont pas les mêmes exigences de performance. Aligner le niveau de compute sur le besoin réel est un levier majeur, en particulier sur Snowflake.
TIERING COMPUTE
│
├── TIER 1 : Interactive ($$$)
│ ├── Analyses ad-hoc des analysts
│ ├── Dashboards temps réel
│ └── Warehouse XS-S, auto-suspend 60s
│
├── TIER 2 : Scheduled ($$)
│ ├── Pipelines ETL/ELT (dbt, Airflow)
│ ├── Rapports planifiés
│ └── Warehouse S-M, auto-suspend 120s
│
└── TIER 3 : Batch ($)
├── Backfills, reprocessing
├── ML training, exports massifs
└── Warehouse M-L, schedule nocturne, auto-suspend 300sNote
Anti-patterns coûteux
Ces patterns sont fréquents et coûtent cher. Je les retrouve dans quasiment chaque audit.
| Anti-pattern | Impact | Solution |
|---|---|---|
| Full-refresh par défaut | 10-100x le coût nécessaire | Incrémental par défaut, full-refresh en exception |
| Warehouse toujours allumé | Paiement 24/7 pour usage 8h | Auto-suspend agressif (60-120s) |
| Pas de partitionnement | Full-scan à chaque requête | Partition par date + require filter |
| Duplication Bronze inutile | 2-3x le storage nécessaire | TTL sur Bronze, dedup en Silver |
| SELECT * dans les views | Colonnes inutiles scannées | Projection explicite, colonnes nommées |
Framework de décision
Avant chaque décision architecturale, posez ces trois questions :
- Quel est le coût récurrent de cette décision ? Un pipeline qui tourne chaque heure a un coût cumulatif bien plus élevé qu'un pipeline quotidien. Calculez le coût annuel, pas le coût unitaire.
- Qui va consommer cette donnée, et comment ? Si 20 personnes vont interroger cette table en ad-hoc, la matérialiser coûte moins cher que de laisser chacun scanner le raw. Si une seule personne l'utilise une fois par mois, une view suffit.
- Quel est le volume dans 6 mois ? Un full-refresh acceptable sur 10 GB devient problématique sur 500 GB. Anticipez la croissance des données dans vos choix d'architecture.
Succes