Une commande ancienne arrive dans l’entrepôt après le traitement du matin. Une autre change de montant. Une troisième est annulée. Le pipeline dbt reste vert, mais le chiffre d’affaires du tableau de bord ne correspond plus aux commandes.
Un modèle incrémental dbt conserve une table et n’en traite qu’une partie à chaque exécution. Sa fiabilité dépend des lignes relues, de leur clé et de la manière d’appliquer les changements. Une ligne éliminée trop tôt par le filtre SQL ne sera jamais corrigée par la stratégie d’écriture.
L’atelier dbt et DuckDB téléchargeable permet de reproduire ce problème, puis de vérifier une solution. Il contient deux modèles, trois jeux de commandes et une recette automatique avec référence métier indépendante. Il a été exécuté localement le 19 octobre 2026 : 12 contrôles réussis, sur des données entièrement fictives. Les dates des scénarios sont elles aussi fictives. Aucun warehouse client ni benchmark de performance n’est impliqué.
Ce que fait un modèle incrémental dbt
À la première construction, dbt matérialise le résultat du modèle. Aux exécutions suivantes, le SQL peut sélectionner seulement les données à reprendre. is_incremental() permet de réserver ce filtre au cas où la table existe déjà, où le modèle est incrémental et où aucun recalcul complet n’est demandé. Le SQL doit rester valable dans les deux chemins. Documentation de configuration dbt.
Il faut distinguer trois décisions :
- Le filtre source détermine quelles lignes entrent dans le traitement.
- La clé
unique_key identifie les lignes de la cible à rapprocher.
- La stratégie incrémentale détermine comment ajouter ou remplacer les données retenues.
Dans notre exemple, le grain est une ligne par commande. La stratégie delete+insert supprime les lignes cibles dont les clés figurent dans le lot entrant, puis insère les nouvelles versions. Le paramètre unique_key='order_id' fournit la correspondance. Il ne constitue pas à lui seul un test d’unicité. Stratégies incrémentales dbt.
L’atelier utilise dbt Core Python 1.12.6, dbt-duckdb 1.11.0 et DuckDB 1.5.6. C’est un environnement local épinglé et exécuté, pas un essai de toutes les capacités de dbt v2 ou State. Notre analyse des annonces dbt traite séparément ces produits.
Trois dates qui ne répondent pas à la même question
La date de commande situe l’activité métier. Une vente du 2 octobre reste une vente du 2 octobre même si elle est intégrée plus tard.
La date de modification source indique quand le système amont dit avoir changé l’enregistrement. Elle est utile si ce système la renseigne correctement pour chaque changement. Mais une commande ancienne peut devenir visible dans l’entrepôt aujourd’hui en conservant sa date de modification ancienne.
La date d’arrivée dans l’entrepôt, nommée ingested_at dans l’atelier, indique quand la version de la ligne a été reçue. Elle doit être mise à jour à chaque remplacement de cette version. Une date attribuée uniquement à la création de la commande ne satisfait pas ce contrat.
Prenons une reprise sur deux jours. Le modèle a déjà vu des modifications du 18 octobre. Il relit seulement les lignes dont source_updated_at est au moins égal à son maximum moins deux jours. Une commande modifiée le 2 octobre, mais chargée le 19, reste hors fenêtre. Ajouter unique_key ne change rien : la ligne n’entre pas dans le lot.
La documentation dbt présente la fenêtre de reprise comme une protection contre certains retards, avec une dérive possible au-delà de cette fenêtre. Elle recommande d’envisager un recalcul complet lorsque les volumes le permettent. Deux jours dans cet atelier est un choix pédagogique, pas une recommandation universelle. Guide dbt sur les modèles incrémentaux.
Le cas des commandes : 170 € affichés, 250 € attendus
Le premier chargement contient trois commandes actives : 100 €, 200 € et 50 €. Total attendu : 350 €. Les montants sont stockés en centimes entiers pour garder les calculs du test exacts.
Le lot suivant contient trois changements :
- La commande 1 passe de 100 € à 120 €.
- La commande 2, de 200 €, est marquée annulée.
- La commande 4, de 80 €, arrive tardivement. Sa date source reste le 2 octobre, son arrivée dans l’entrepôt est datée du 19.
La commande 3 reste à 50 €. La référence métier donne donc 120 + 50 + 80 = 250 €. Elle est écrite explicitement dans le script de contrôle, et ne provient pas d’une somme recopiée depuis le modèle testé.
Le contre-exemple orders_unsafe filtre sur la date source. Il reprend la correction et l’annulation, mais manque la commande 4 : 170 €. Le modèle orders_current utilise la date d’arrivée de chaque version et retrouve les quatre enregistrements, dont trois actifs : 250 €.
L’annulation reste une ligne marquée is_deleted=true. La vue de chiffre d’affaires l’exclut au moment du calcul. La supprimer du lot avant la mise à jour laisserait l’ancienne ligne active dans la cible. Le mot « delete » dans delete+insert ne signifie pas que toute ligne disparue de la source sera automatiquement supprimée. La gestion des suppressions physiques nécessite une règle distincte. Choisir entre incrémental et snapshots.
Tutoriel : exécuter la recette dans un environnement isolé
1. Installer les versions de l’atelier
Prérequis : Python 3.12, un terminal et un accès Internet pour télécharger les dépendances. Aucun compte dbt hébergé, aucune clé cloud et aucun service payant ne sont nécessaires à cet exercice local. Les ressources machine restent à votre charge.
Téléchargez l’archive, décompressez-la et placez-vous dans le dossier atelier :
python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements-lock.txt
python run_demo.py --output rapport.json
Sur Windows, l’activation se fait avec .venv\Scripts\Activate.ps1 dans PowerShell. Le script lance le véritable exécutable dbt de cet environnement. Il travaille dans un dossier temporaire, y crée sa propre base DuckDB et nettoie ce dossier à la fin. Le rapport demandé reste disponible. Il ne se connecte à aucune base de votre entreprise.
Les versions directes figurent dans requirements.txt ; le verrou conserve aussi les dépendances résolues lors de notre exécution. Le projet officiel dbt-duckdb documente les stratégies disponibles : delete+insert est notre choix pour rendre le remplacement explicite. merge est également supporté avec DuckDB 1.4 ou supérieur ; il n’est pas exécuté dans cette recette.
2. Lire le modèle avant de modifier le filtre
Le modèle principal est volontairement court :
{{ config(materialized='incremental', unique_key='order_id',
incremental_strategy='delete+insert') }}
select order_id, order_date, amount_cents, source_updated_at,
ingested_at, is_deleted
from {{ ref('raw_orders') }}
{% if is_incremental() %}
where ingested_at >= (
select coalesce(max(ingested_at), timestamp '1900-01-01')
- interval '2 days'
from {{ this }}
)
{% endif %}
Le >= relit aussi la frontière de la fenêtre. coalesce couvre une cible vide. La clé permet de remplacer une ligne déjà présente lors de ce rejeu. Les fichiers source représentent l’état courant, unique par commande ; ce modèle ne déduplique pas un journal contenant plusieurs versions de la même clé.
Pour adapter le modèle à un journal de changements, il faut d’abord définir l’ordre fiable des versions et construire l’état à retenir. Une égalité de timestamps ne suffit pas toujours à départager deux événements. Ce travail relève du contrat d’ingestion et de la recette CDC, avant la transformation analytique.
3. Vérifier les clés avant d’écrire la cible
Le script charge chaque jeu avec dbt seed, puis exécute les tests de la source avant les modèles. Il s’arrête si un test échoue. La configuration comprend notamment :
columns:
- name: order_id
data_tests: [not_null, unique]
- name: ingested_at
data_tests: [not_null]
Un doublon est injecté à la fin de la recette. Le test d’unicité échoue volontairement, aucun nouveau traitement de modèle n’est lancé, et la cible reste identique. L’échec d’un test après matérialisation n’annule pas nécessairement les écritures déjà effectuées. L’ordre des étapes et le blocage de diffusion doivent être explicites dans votre orchestration.
4. Contrôler les lignes, le rejeu et les totaux journaliers
Le rapport doit avoir status: passed et douze contrôles réussis. Parmi eux :
- Premier chargement : 35 000 centimes.
- Corrections et arrivée tardive : quatre lignes conservées, dont l’annulation, et 25 000 centimes actifs.
- Contre-exemple : commande 4 absente et total de 17 000 centimes.
- Rejeu du même lot : toutes les colonnes de la table restent identiques.
- Nouvelle correction de la commande 3 : 6 000 centimes, total actif de 26 000.
- Recalcul complet : mêmes lignes et valeurs que le résultat incrémental.
Cette seconde correction garde volontairement une date source ancienne et reçoit une nouvelle date d’arrivée. Elle teste donc le contrat choisi, sans affirmer que toutes les applications métier se comportent ainsi.
La vue daily_revenue est recalculée à la lecture sur l’état courant. Sa référence finale attend 120 € au 1er octobre, 80 € au 2 et 60 € au 18. Elle évite, dans ce petit exemple, un second problème : conserver un agrégat journalier ancien après correction d’une commande. Pour un agrégat lui-même incrémental, reprenez les groupes affectés, y compris l’ancien groupe si une ligne change de date ou de catégorie.
Ce que cette solution exige en production
L’horodatage d’arrivée est une précondition, pas une garantie apportée par dbt. Une ingestion qui réutilise l’ancienne valeur lors d’un backfill peut encore masquer une correction. Une date future erronée peut déplacer le maximum et exclure d’autres lignes. Contrôlez ces anomalies et la visibilité des lots avant de vous fier au maximum de la cible.
Définissez également la durée de conservation amont. Un --full-refresh ne recrée pas une donnée que la source ne conserve plus. Le test local compare bien les deux chemins, mais il utilise une source complète ; il ne démontre pas la possibilité de reconstruire votre historique réel.
Trois usages demandent des choix différents :
- Ventes corrigées. Retenir un état courant par commande, intégrer les annulations et rapprocher les montants avec une référence finance. Succès : mêmes clés, statuts et valeurs sur une période fermée.
- Stocks recalculés. Un inventaire corrige parfois une position ancienne. Définir le grain article, lieu et date, puis reprendre les partitions affectées. Succès : concordance des positions et absence de double comptage après rejeu.
- Historique client. Conserver la version précédente d’un attribut répond à un autre besoin. Un remplacement incrémental ne constitue pas un historique ; étudier les snapshots ou une historisation explicite selon les données disponibles. Succès : périodes cohérentes et version correcte à la date demandée.
Choisir une fenêtre et mesurer le coût réel
Avant de réduire les volumes relus, mesurez la distribution des retards, les corrections les plus anciennes et les durées de reconstruction. Une moyenne de retard faible peut cacher les quelques lignes qui faussent une clôture mensuelle. Décidez avec le métier du délai acceptable et du mécanisme de réparation au-delà de ce délai.
L’incrémental vise à éviter des recalculs inutiles. Le gain dépend du moteur, des filtres réellement appliqués, du plan de requête, du stockage et des opérations sur la cible. Un filtre ne prouve pas à lui seul une baisse du nombre d’octets lus. Mesurez durée, données scannées, coût facturé, fréquence des réparations et écarts à la référence, sur les mêmes données.
Pour une petite table reconstruite rapidement, conserver le recalcul complet peut simplifier l’exploitation. Pour une table volumineuse et fréquemment corrigée, une stratégie incrémentale devient intéressante si l’équipe sait rejouer les lots, reprendre les anciennes périodes et surveiller les écarts. Le guide sur l’orchestration des pipelines complète cette partie exploitation.
Commencez par une seule table mutable, dans un environnement isolé. Introduisez une arrivée tardive, une correction ancienne et une annulation. Rejouez le lot. Comparez ensuite toutes les lignes à une référence validée et à une reconstruction complète. Le critère de passage est la concordance des données attendues ; la réduction du temps et du coût vient se mesurer ensuite.