dbt Incremental Models vs Snowflake Dynamic Tables
Résumé
Deux façons de maintenir une table de faits à jour sans tout recalculer. Un modèle incrémental dbt exécute une logique que vous écrivez : quelles lignes traiter, comment les fusionner. Une Snowflake Dynamic Table reçoit seulement la requête de l’état voulu et un retard maximal toléré (
target_lag) ; Snowflake décide du reste. La première donne du contrôle et de la portabilité au prix de cas limites à coder soi-même. La seconde propage naturellement les changements amont, au prix d’une dépendance à Snowflake et de limites sur la requête.
Prérequis
dbt — Fondamentaux pour les matérialisations et
ref(). Le glossaire définit CDC et idempotence : Glossaire data engineering.
Le scénario
stg_events (clickstream, des millions de lignes par jour, souvent en retard à cause de connexions instables) est joint à stg_users (dimension mutable : email, âge, centres d’intérêt, avec des suppressions liées au RGPD). Le résultat, fct_user_events, doit être rafraîchi toutes les 20 minutes. Trois cas doivent être corrects :
- les événements qui arrivent en retard ;
- les mises à jour de la dimension qui doivent se refléter sur d’anciens événements ;
- les suppressions en amont.
Comparaison
| Aspect | Modèle incrémental dbt | Snowflake Dynamic Table |
|---|---|---|
| Ce que vous écrivez | La logique de sélection et de fusion | La requête de l’état final et le retard toléré |
| Événements en retard | Fenêtre de lookback à choisir | Suivis par le moteur, sans fenêtre à régler |
| Dimension qui change | Seconde fenêtre à prévoir | Reflétée par le suivi des changements |
| Suppression en amont | Non gérée par merge | Reflétée selon la jointure, voir plus bas |
| Déclenchement | dbt run, au moment choisi | Géré par Snowflake selon target_lag, ou manuel |
| Portabilité | SQL, mais merge et insert_overwrite dépendent de l’adapter | Snowflake uniquement |
| Logique libre | Totale | Une seule requête select, avec des restrictions |
Approche 1 : modèle incrémental
-- models/fct_user_events.sql
{{ config(materialized='incremental', unique_key='event_id', incremental_strategy='merge') }}
select e.event_id, e.user_id, u.user_email, e.event_name, e.event_timestamp
from {{ ref('stg_events') }} as e
left join {{ ref('stg_users') }} as u on e.user_id = u.user_id
{% if is_incremental() %}
where e.event_timestamp >= (select max(event_timestamp) from {{ this }}) - interval '3 days'
{% endif %}On filtre les faits sur une fenêtre de lookback (ici 3 jours) pour rattraper les retards. La dimension n’est pas filtrée dans ce code, mais un email modifié ne corrige pas les événements plus anciens que la fenêtre.
Ce qui casse
- Fenêtre arbitraire. Trop courte : des événements en retard sont perdus sans erreur. Trop longue : on relit des jours de données à chaque run. Exemple chiffré : 5 millions d’événements par jour, fenêtre de 3 jours, un run toutes les 20 minutes, soit 72 runs par jour. Chaque run relit environ 15 millions de lignes, donc plus d’un milliard de lignes lues par jour pour environ 5 millions de nouvelles.
- Dimension mise à jour. Si
stg_usersest filtrée sur la même fenêtre, la mise à jour d’un email ne corrige pas les événements plus anciens que la fenêtre. - Suppressions ignorées.
mergeinsère ou met à jour, jamais ne supprime. Un utilisateur effacé en amont reste dans la table finale, sauf traitement séparé (post-hook, modèle dédié).
Ces trois cas sont normaux en production, pas des cas limites.
Approche 2 : Dynamic Table
-- models/fct_user_events_dynamic.sql
{{ config(materialized='dynamic_table', target_lag='20 minutes', snowflake_warehouse='transform_wh') }}
select e.event_id, e.user_id, u.user_email, e.event_name, e.event_timestamp
from {{ ref('stg_events') }} as e
left join {{ ref('stg_users') }} as u on e.user_id = u.user_idPas de is_incremental(), pas de unique_key, pas de fenêtre. Snowflake suit les changements des tables sources (change tracking) et met la table à jour.
target_lagest un objectif de fraîcheur, pas un planning.'20 minutes'signifie que les données ne devraient pas dépasser ce retard ; Snowflake choisit la cadence réelle. Avec la planification automatique, le paramètre est obligatoire à la création : une durée, ouDOWNSTREAM(la table se rafraîchit selon les besoins de ses tables consommatrices). Il existe aussiSCHEDULER = DISABLE: le rafraîchissement est alors déclenché de l’extérieur, par exemple par dbt (la documentation Snowflake précise l’usage deTARGET_LAGdans ce mode).- Ce n’est pas une vue matérialisée. Les vues matérialisées Snowflake se limitent à une table source et à peu d’agrégations ; une Dynamic Table accepte jointures, CTE, fonctions de fenêtrage.
Le mode de rafraîchissement change tout
Une Dynamic Table se rafraîchit en mode incrémental (seulement les changements) ou complet (tout recalculer). Le paramètre
REFRESH_MODEaccepteAUTO,FULL,INCREMENTALet d’autres valeurs plus récentes ; Snowflake recommande de le fixer explicitement. EnAUTO, certaines requêtes retombent en mode complet, et l’économie par rapport à un modèle dbt qui relit une fenêtre disparaît alors. Vérifier le mode effectif de chaque table (SHOW DYNAMIC TABLES, colonnerefresh_mode) et la liste des restrictions de la documentation Snowflake.
Une suppression n'est pas toujours une suppression
Avec le
left joinci-dessus, effacer un utilisateur destg_usersne supprime pas ses événements : la colonneuser_emaildevientNULL. Pour retirer les lignes, il faut une jointure interne ou un filtre explicite. Et la donnée effacée reste récupérable pendant la durée du Time Travel puis du Fail-safe de Snowflake : l’effacement n’est complet qu’après leur expiration.
Détails opérationnels
Orchestration
dbt émet seulement create ou alter dynamic table ; le rafraîchissement réel est asynchrone. Pour maîtriser le moment, on peut fixer un target_lag long ou DOWNSTREAM, puis déclencher un rafraîchissement manuel :
ALTER DYNAMIC TABLE fct_user_events_dynamic REFRESH;Cela peut se placer dans un post-hook dbt.
Tests
Les Dynamic Tables sont des objets ordinaires : dbt test fonctionne. Forcer un rafraîchissement avant de tester, sinon le résultat dépend du moment (table pas encore à jour). Règle stricte en CI.
Coûts
- Un
target_lagtrès court (1 minute) garde l’entrepôt actif : crédits de calcul et de Cloud Services. Adapter le retard au besoin réel. - Utiliser un entrepôt dédié pour attribuer correctement les coûts.
- Un coût de Cloud Services élevé par rapport au calcul suggère un
target_lagtrop agressif : à auditer avecWAREHOUSE_METERING_HISTORY.
Pannes
- Après 5 échecs consécutifs de rafraîchissements planifiés, la table passe en
SUSPENDED_DUE_TO_ERRORS. Un rafraîchissement manuel n’incrémente pas ce compteur, et un succès le remet à zéro. - Si une table amont est suspendue, les tables aval cessent de se rafraîchir sans avoir échoué.
- Corriger la cause ne relance rien : la table suspendue se reprend avec
ALTER DYNAMIC TABLE … RESUME. Cette commande se propage aux tables aval suspendues en cascade ; seules celles suspendues manuellement (USER_SUSPENDED) se reprennent à la main. - Détecter les suspensions avec
INFORMATION_SCHEMA.DYNAMIC_TABLE_GRAPH_HISTORY(): les champsreason_codeetreason_messagesont imbriqués dans l’objetscheduling_state. Envelopper cela dans une alerte planifiée.
Choisir
| Critère | Incrémental dbt | Dynamic Table |
|---|---|---|
| Garder la possibilité de changer d’entrepôt | Mieux, avec réserve sur les stratégies d’adapter | Non |
| Déclenchement synchronisé à un événement externe | Oui | Possible avec rafraîchissement manuel |
| Logique procédurale, appels externes, fonctions non déterministes | Oui | Non, ou rafraîchissement complet |
| Propagation des changements de dimension | À coder | Native, si le mode incrémental est actif |
| Réglage de la fenêtre de lookback | À faire | Inutile |
Pistes, pas règle
Des équipes réservent les Dynamic Tables aux jointures volumineuses où le suivi des changements fait l’essentiel du travail, et gardent des modèles incrémentaux pour les marts agrégés ou à logique libre. Cela reste une option à valider sur la charge réelle, notamment le mode de rafraîchissement effectif. Elle est en tension avec la convention qui met le staging en
view.
À vérifier avant de s'appuyer dessus
Les restrictions exactes du rafraîchissement incrémental, les valeurs par défaut de
refresh_modeet la durée de rétention du Time Travel dépendent de la version et de l’édition de Snowflake. Contrôler dans la documentation Snowflake en vigueur.
Sources
- Snowflake, gérer les Dynamic Tables (suspension, reprise).
- Snowflake, CREATE DYNAMIC TABLE (
TARGET_LAG,SCHEDULER,REFRESH_MODE). - Snowflake, DYNAMIC_TABLE_GRAPH_HISTORY.
Notes liées
- dbt — Fondamentaux : la matérialisation
incrementaldans son contexte. - Idempotence et backfill : rejouer une tranche sans doublons.
- dbt et le lakehouse : les stratégies incrémentales selon le format de table et le moteur.
- dbt_bigquery_monitoring : surveiller le coût d’un projet dbt, côté BigQuery.
- Glossaire data engineering : CDC, idempotence.
- MOC Data Engineering : carte du vault.