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 :

  1. les événements qui arrivent en retard ;
  2. les mises à jour de la dimension qui doivent se refléter sur d’anciens événements ;
  3. les suppressions en amont.

Comparaison

AspectModèle incrémental dbtSnowflake Dynamic Table
Ce que vous écrivezLa logique de sélection et de fusionLa requête de l’état final et le retard toléré
Événements en retardFenêtre de lookback à choisirSuivis par le moteur, sans fenêtre à régler
Dimension qui changeSeconde fenêtre à prévoirReflétée par le suivi des changements
Suppression en amontNon gérée par mergeReflétée selon la jointure, voir plus bas
Déclenchementdbt run, au moment choisiGéré par Snowflake selon target_lag, ou manuel
PortabilitéSQL, mais merge et insert_overwrite dépendent de l’adapterSnowflake uniquement
Logique libreTotaleUne 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_users est 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. merge insè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_id

Pas 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_lag est 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, ou DOWNSTREAM (la table se rafraîchit selon les besoins de ses tables consommatrices). Il existe aussi SCHEDULER = DISABLE : le rafraîchissement est alors déclenché de l’extérieur, par exemple par dbt (la documentation Snowflake précise l’usage de TARGET_LAG dans 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_MODE accepte AUTO, FULL, INCREMENTAL et d’autres valeurs plus récentes ; Snowflake recommande de le fixer explicitement. En AUTO, 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, colonne refresh_mode) et la liste des restrictions de la documentation Snowflake.

Une suppression n'est pas toujours une suppression

Avec le left join ci-dessus, effacer un utilisateur de stg_users ne supprime pas ses événements : la colonne user_email devient NULL. 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_lag trè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_lag trop agressif : à auditer avec WAREHOUSE_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 champs reason_code et reason_message sont imbriqués dans l’objet scheduling_state. Envelopper cela dans une alerte planifiée.

Choisir

CritèreIncrémental dbtDynamic Table
Garder la possibilité de changer d’entrepôtMieux, avec réserve sur les stratégies d’adapterNon
Déclenchement synchronisé à un événement externeOuiPossible avec rafraîchissement manuel
Logique procédurale, appels externes, fonctions non déterministesOuiNon, ou rafraîchissement complet
Propagation des changements de dimensionÀ coderNative, si le mode incrémental est actif
Réglage de la fenêtre de lookbackÀ faireInutile

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.

Sources

Notes liées