Dans l'article précédent, on a vu le chargement incremental, comment charger efficacement les nouvelles lignes, et mettre à jour les anciennes. Mais cette mise à jour a un défaut pour certains besoins : elle écrase l'ancienne valeur.
Un client déménage de Paris à Lyon. Avec un upsert classique, sa ligne passe à Lyon et Paris disparaît à jamais. Or parfois, savoir qu'il habitait Paris jusqu'au 15 mars compte vraiment (analyses historiques, conformité, reporting daté etc...). C'est le rôle du SCD Type 2, et dbt l'implémente nativement avec les snapshots. La théorie du SCD est dans mon article dédié , donc ici, on l'implémente sans écrire une ligne de SQL d'historisation.
Ce qu'est un snapshot
Un snapshot, c'est une photo de l'état d'une table prise à chaque exécution. dbt compare la photo du jour à la précédente, détecte les lignes qui ont changé, expire l'ancienne version et insère la nouvelle.
Pour gérer ça, dbt ajoute quatre colonnes techniques à la table de snapshot :
dbt_valid_from: depuis quand cette version est validedbt_valid_to: jusqu'à quand. La version courante a undbt_valid_toàNULLdbt_scd_id: un identifiant unique par versiondbt_updated_at: la date et time de la capture
Avec ces colonnes, tu as un historique complet, donc chaque ligne du passé est conservée, et la ligne active est celle dont dbt_valid_to est NULL.
Les deux stratégies de détection
dbt propose deux façons de repérer qu'une ligne a changé :
- Timestamp : tu indiques une colonne qui change à chaque modification (un
updated_atpar exemple). dbt compare cette date. C'est la stratégie la plus performante, à utiliser dès que la source a une date d'update fiable. - Check : tu listes les colonnes à surveiller (
check_cols). dbt compare leurs valeurs d'une exécution à l'autre. À utiliser quand la source n'a pas d'updated_atfiable.
Notre raw.customers n'a pas de colonne de mise à jour propre, donc on part sur la stratégie check mais le principe reste le même avec le timestamp.
Notre premier snapshot
Crée un fichier snapshots/customers_snapshot.yml :
snapshots:
- name: customers_snapshot
relation: source('raw', 'customers')
config:
unique_key: id
strategy: check
check_cols: ['first_name', 'last_name', 'email']
L'idée est de dire à dbt "surveille la table source raw.customers, identifie chaque client par id, et considère qu'une ligne a changé si first_name, last_name ou email change".
.sql avec un bloc {% snapshot %} et une config() Jinja, c'est l'ancienne méthode. Depuis dbt 1.9, elle est considérée comme legacy. La bonne façon aujourd'hui, c'est le fichier YAML avec la clé snapshots: et le champ relation, comme ci-dessus.On remarque aussi qu'on snapshote la source brute (source('raw', 'customers')), pas un modèle de staging. C'est la bonne pratique car on historise la donnée au plus près de son arrivée, avant toute transformation.
Lance la capture :
dbt snapshot
dbt crée la table customers_snapshot avec tes trois clients et les quatre colonnes techniques. Va voir dans Snowsight : chaque ligne a un dbt_valid_from rempli et un dbt_valid_to à NULL, puisque ce sont les versions courantes.

updated_at), tu utiliserais la stratégie timestamp à la place de check. Tu remplaces alors check_cols par updated_at, qui indique la colonne à surveiller et la strategy 'check' par 'timestamp'.Voir l'historique se créer
C'est maintenant que la magie opère. Simule un changement, par exemple le client 1 change d'email.
UPDATE raw.customers
SET email = 'marc.dubois@nouvelle-adresse.fr'
WHERE id = 1;
Relance la capture :
dbt snapshot
Va interroger la table de snapshot pour le client 1 :
select id, email, dbt_valid_from, dbt_valid_to
from analytics.dev.customers_snapshot
where id = 1
order by dbt_valid_from;
Tu as maintenant deux lignes pour le client 1 :

- L'ancienne, avec l'ancien email, dont le
dbt_valid_toest désormais rempli (la version est expirée), - La nouvelle, avec le nouvel email, dont le
dbt_valid_toest àNULL(la version courante).
Voilà ton SCD Type 2. Tu n'as écrit aucune logique d'expiration ni d'insertion, dbt a tout fait à partir de ton YAML. Et tu peux maintenant répondre par exemple à "quel était l'email de ce client avant le 15 mars ?".
check, les dates dbt_valid_from et dbt_valid_to correspondent au moment où dbt observe le changement, pas forcément à la vraie date métier du changement.Gérer les suppressions
Une question revient vite, c'est que se passe-t-il si une ligne disparaît de la source ? Par défaut, dbt la laisse telle quelle dans le snapshot, avec son dbt_valid_to toujours à NULL, comme si elle était encore active. Si tu veux qu'une disparition soit traitée comme une fin de validité, ajoute la config hard_deletes :
config:
unique_key: id
strategy: check
check_cols: ['first_name', 'last_name', 'email']
hard_deletes: invalidate
Avec invalidate, une ligne supprimée de la source voit son dbt_valid_to rempli à la prochaine capture. C'est la config moderne qui remplace l'ancien invalidate_hard_deletes.
delete from raw.customers where id=1;
Relance :
dbt snapshot

Sous le capot, et quand l'utiliser
Comme le chargement incremental, un snapshot s'appuie sur un MERGE généré par dbt pour expirer et insérer les versions. Si tu veux comprendre la mécanique, c'est le même principe que j'ai détaillé dans l'article MERGE SQL.
Si tu te poses la question, c'est snapshot ou incrémental ? La réponse c'est ni l'un ni l'autre car ce sont deux besoins différents.
| Besoin | Outil |
|---|---|
| Charger efficacement de gros volumes qui grossissent par ajout (faits, événements) | Modèle incremental |
| Garder l'historique des changements d'une dimension qui évolue (SCD Type 2) | snapshot |
L'incremental optimise le chargement. Le snapshot capture l'histoire. On les utilise souvent ensemble dans un même projet, sur des tables différentes.
Côté exécution, le snapshot a sa propre commande, dbt snapshot, mais il est aussi inclus quand tu lances dbt build. En prod, on le planifie au bon rythme car trop espacé, tu risques de rater des changements entre deux captures.
Ce qu'on a fait, et la suite
Récap. de cet article :
- Comprendre pourquoi un upsert classique écrase l'historique, et ce que le SCD Type 2 résout
- Définir un snapshot en YAML avec la stratégie
check(dbt 1.9+), - Voir une deuxième version de ligne se créer après un changement, avec ses dates de validité
- Gérer les suppressions avec
hard_deletes - Poser la distinction claire entre snapshot (historiser) et incremental (charger).
Tu sais maintenant historiser tes dimensions proprement. Dans le prochain article, on attaque le sujet qui fait passer de quelqu'un qui écrit du SQL dans dbt à un vrai engineer : Jinja et les macros, pour arrêter de te répéter et factoriser ta logique.
Aller plus loin
Cet article fait partie de la formation dbt complète, du premier modèle au déploiement en production.
👉 Suivre toute la formation dbt
dbt tourne sur Snowflake dans ce parcours. Pour maîtriser le socle (warehouses, rôles, ingestion) :
👉 Accéder à la Formation Snowflake
Et pour t'entraîner en conditions d'examen, la certification dbt Analytics Engineering teste précisément les snapshots et le SCD Type 2.
👉 Préparer la certification dbt sur DataCertification.fr
Tu veux que je t'accompagne sur ton projet data (Snowflake, dbt, modélisation, industrialisation) ?
👉 Réserver un appel de 30 minutes
Questions fréquentes
C'est quoi un snapshot dbt ?
Un snapshot est un mécanisme dbt qui capture l'état d'une table à chaque exécution pour en garder l'historique. Quand une ligne change, dbt expire l'ancienne version et insère la nouvelle, avec des dates de validité. C'est l'implémentation native du SCD Type 2 donc on conserve chaque version d'une donnée au lieu de l'écraser.
Quelle différence entre la stratégie timestamp et check ?
La stratégie timestamp s'appuie sur une colonne horodatée fiable (un updated_at) qui change à chaque modification et donc dbt compare cette date. La stratégie check compare les valeurs des colonnes que tu listes dans check_cols. On utilise timestamp quand la source a un horodatage fiable, et check quand elle n'en a pas.
C'est quoi les colonnes dbt_valid_from et dbt_valid_to ?
Ce sont deux colonnes que dbt ajoute automatiquement à un snapshot pour gérer l'historique. dbt_valid_from indique depuis quand une version de la ligne est valide, et dbt_valid_to jusqu'à quand. La version courante d'une ligne a un dbt_valid_to à NULL. C'est ce couple de dates qui permet de retrouver l'état d'une donnée à n'importe quelle date passée.
Comment définir un snapshot en dbt 1.9 et plus ?
Dans un fichier YAML placé dans le dossier snapshots, avec la clé snapshots, le champ relation (la source ou le modèle à historiser) et un bloc config (unique_key, strategy, et selon la stratégie updated_at ou check_cols). L'ancienne méthode avec un bloc Jinja snapshot dans un fichier .sql est devenue legacy depuis dbt 1.9.
Snapshot ou modèle incremental, quand utiliser quoi ?
Un modèle incremental sert à charger efficacement de gros volumes qui grossissent par ajout, comme des faits ou des événements. Un snapshot sert à garder l'historique des changements d'une dimension qui évolue, c'est-à-dire le SCD Type 2. L'incremental optimise le chargement, le snapshot capture l'histoire. Les deux coexistent souvent dans un même projet sur des tables différentes.
