Un modèle idempotent produit des résultats identiques quel que soit le nombre de fois qu’il s’exécute. La plupart des modèles incrémentaux ne sont pas idempotents par défaut, et les défaillances sont subtiles — elles passent généralement inaperçues jusqu’à un audit de qualité des données.
Pourquoi les modèles incrémentaux brisent l’idempotence
La façon la plus courante de briser l’idempotence : se fier uniquement à unique_key pour la déduplication.
Avec la stratégie merge, unique_key indique à dbt comment faire correspondre les lignes entrantes avec les lignes existantes. Les lignes correspondantes sont mises à jour ; les lignes sans correspondance sont insérées. Cela fonctionne lors des exécutions incrémentales — mais la première exécution (ou tout --full-refresh) n’utilise pas du tout la logique de merge. Elle se comporte comme une matérialisation de table : tout insérer, sans déduplication.
Si vos données source contiennent des doublons et que votre modèle s’appuie sur unique_key pour les gérer, la première exécution insère tous les doublons. Les exécutions incrémentales suivantes peuvent en mettre à jour certains, mais les doublons initiaux persistent. Votre table contient maintenant des lignes qui n’existeraient pas si vous aviez exécuté le modèle de manière incrémentale depuis le début.
-- Ce modèle N'EST PAS idempotent{{ config( materialized='incremental', unique_key='event_id', incremental_strategy='merge') }}
SELECT event_id, event_timestamp, user_id, event_typeFROM {{ ref('base__analytics__events') }}{% if is_incremental() %}WHERE event_timestamp > (SELECT MAX(event_timestamp) FROM {{ this }}){% endif %}Si base__analytics__events contient deux lignes avec le même event_id, le full refresh insère les deux. La stratégie merge aurait détecté cela lors d’une exécution incrémentale, mais la première exécution contourne entièrement le merge.
La correction : dédupliquer dans le SELECT
Incluez une déduplication explicite dans votre SELECT afin qu’elle s’applique aussi bien lors d’un full refresh que lors d’exécutions incrémentales :
{{ config( materialized='incremental', unique_key='event_id', incremental_strategy='merge') }}
SELECT event_id, event_timestamp, user_id, event_type, event_propertiesFROM {{ ref('base__analytics__events') }}{% if is_incremental() %}WHERE event_timestamp >= ( SELECT MAX(event_timestamp) - INTERVAL 3 DAY FROM {{ this }}){% endif %}QUALIFY ROW_NUMBER() OVER ( PARTITION BY event_id ORDER BY event_timestamp DESC) = 1La clause QUALIFY ROW_NUMBER() s’exécute à chaque exécution — full refresh ou incrémentale. Elle sélectionne la version la plus récente de chaque event_id avant que les données n’atteignent l’étape de merge. Aucun doublon n’entre dans la table de staging, donc aucun doublon n’entre dans la cible.
Ce pattern fonctionne sur BigQuery, Snowflake et Databricks. Sur les entrepôts qui ne supportent pas la syntaxe QUALIFY, utilisez un CTE :
WITH deduplicated AS ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY event_id ORDER BY event_timestamp DESC ) AS rn FROM {{ ref('base__analytics__events') }} {% if is_incremental() %} WHERE event_timestamp >= ( SELECT MAX(event_timestamp) - INTERVAL 3 DAY FROM {{ this }} ) {% endif %})
SELECT * EXCEPT(rn)FROM deduplicatedWHERE rn = 1Les valeurs NULL dans unique_key brisent également l’idempotence
Les valeurs NULL dans les colonnes unique_key provoquent des échecs de correspondance lors des opérations de merge. En SQL, NULL = NULL s’évalue à NULL (pas à TRUE), donc deux lignes avec NULL dans une colonne unique_key ne correspondent jamais. Au lieu de mettre à jour la ligne existante, dbt insère une nouvelle ligne. Exécutez le modèle à nouveau avec les mêmes données et vous obtenez un autre doublon. Chaque exécution en ajoute davantage.
-- Clé composite où session_number peut être NULL{{ config(unique_key=['user_id', 'session_number']) }}
-- Si session_number est NULL pour certaines lignes, ces lignes-- ne correspondront JAMAIS lors du merge, créant des doublons à chaque exécutionLa correction : assurez-vous qu’aucune colonne de unique_key n’est nulle. Soit filtrez les NULL, soit remplacez-les par une valeur sentinelle avec coalesce, soit choisissez un unique_key différent qui n’inclut pas de colonnes nullables.
{{ config(unique_key='surrogate_key') }}
SELECT {{ dbt_utils.generate_surrogate_key(['user_id', 'coalesce(session_number, -1)']) }} AS surrogate_key, user_id, session_number, ...Fenêtres de lookback et idempotence
Le pattern de fenêtre de lookback interagit avec l’idempotence de manière importante. Si votre lookback retraite 3 jours de données et que votre déduplication sélectionne la version la plus récente de chaque enregistrement, exécuter le modèle plusieurs fois produit le même résultat : la même fenêtre de 3 jours est retraitée, la même logique de déduplication s’applique, et les mêmes lignes atterrissent dans la cible.
Mais si votre lookback utilise MAX(event_timestamp) FROM {{ this }} comme ancre et que le timestamp maximum de la table change entre les exécutions (parce que de nouvelles données sont arrivées), la fenêtre de lookback se déplace. C’est un comportement attendu, pas une violation de l’idempotence — cela signifie que le modèle produit des résultats corrects pour l’état actuel des données, pas qu’il produit un output identique quel que soit le moment de l’exécution.
La vraie idempotence signifie : étant donné les mêmes données source, produire le même output quel que soit le nombre d’exécutions. La fenêtre de lookback garantit que les données arrivant tardivement dans la fenêtre sont correctement intégrées, et la déduplication préalable garantit que les doublons dans les données source ne se propagent pas.
Tester l’idempotence
Le test le plus direct : exécutez votre modèle deux fois de suite sans modifier les données source et comparez les nombres de lignes. Si le nombre change, le modèle n’est pas idempotent.
dbt run --select my_model# Enregistrer le nombre de lignesdbt run --select my_model# Comparer le nombre de lignes — devrait être identiqueLe framework de tests unitaires de dbt peut valider l’idempotence de manière plus rigoureuse. Créez des fixtures de test avec des doublons connus et vérifiez que l’output du modèle ne contient que des lignes dédupliquées, que le modèle s’exécute en full refresh ou de manière incrémentale.
Pour les modèles utilisant la stratégie append, l’idempotence requiert un traitement différent car append ne déduplique jamais. Soit acceptez les doublons et traitez-les en aval, soit n’utilisez pas append pour des données susceptibles de contenir des doublons.
La checklist
- Dédupliquer dans votre SELECT (QUALIFY ou ROW_NUMBER), pas uniquement via
unique_key - S’assurer qu’il n’y a pas de NULL dans les colonnes
unique_key - Tester les modes full-refresh et incrémental pour vérifier la cohérence des résultats
- Si vous utilisez des fenêtres de lookback, vérifier que les données retraitées se fusionnent correctement
- Pour la stratégie append, gérer la déduplication en aval