ServicesÀ proposNotesContact Me contacter →
EN FR
Note

Tester la gestion des données tardives dans dbt

Comment écrire des tests unitaires dbt qui simulent les arrivées tardives, et comment utiliser audit_helper pour détecter la dérive entre les résultats incrémentaux et les full-refresh en production.

Planté
dbtincremental processingdata qualitytesting

L’idempotence et les fenêtres de rattrapage ne fonctionnent que si vous les testez. Sans tests explicites, une fenêtre de rattrapage mal configurée rate silencieusement des enregistrements tardifs pendant des mois avant que quelqu’un ne s’en aperçoive. À ce stade, la dérive est difficile à quantifier et encore plus difficile à corriger.

Il existe deux couches de test : les tests unitaires qui vérifient que votre logique de modèle gère correctement les arrivées tardives, et les requêtes de comparaison en production qui détectent la dérive entre les résultats incrémentaux et les full-refresh.

Tests unitaires des arrivées tardives avec dbt 1.8+

Le framework de tests unitaires de dbt (ajouté dans dbt 1.8) vous permet de simuler l’état de votre table cible et de vérifier que votre modèle gère correctement les enregistrements arrivant tardivement. La clé est de surcharger is_incremental() pour forcer le modèle en mode incrémental pendant le test.

unit_tests:
- name: test_late_arrival_captured_within_window
model: fct__events
overrides:
macros:
is_incremental: true
given:
- input: this
rows:
- {event_id: 1, event_data: "original", updated_at: "2024-01-15"}
- {event_id: 3, event_data: "existing_record", updated_at: "2024-01-14"}
- input: ref('base__events')
rows:
- {event_id: 1, event_data: "updated", updated_at: "2024-01-17"}
- {event_id: 2, event_data: "late_arrival", updated_at: "2024-01-13"}
- {event_id: 3, event_data: "existing_record", updated_at: "2024-01-14"}
expect:
rows:
- {event_id: 1, event_data: "updated"}
- {event_id: 2, event_data: "late_arrival"}
- {event_id: 3, event_data: "existing_record"}

Ce que ce test vérifie : event_id: 1 est mis à jour (sa version plus récente remplace l’enregistrement existant), event_id: 2 est une arrivée tardive datée du 13 janvier — avant le maximum actuel du 17 janvier dans {{ this }} — et il est capturé car il tombe dans la fenêtre de rattrapage, et event_id: 3 passe inchangé.

La surcharge is_incremental: true est essentielle. Sans elle, le modèle s’exécute en mode full-refresh et la référence {{ this }} n’a aucune signification, rendant le test inutile pour vérifier le comportement incrémental.

Tester les conditions aux limites

La limite de la fenêtre de rattrapage est l’endroit où se trouvent la plupart des bugs. Un enregistrement qui arrive exactement à la limite devrait être capturé ; un qui arrive juste au-delà ne devrait pas l’être. Testez les deux :

unit_tests:
- name: test_record_at_lookback_boundary_captured
model: fct__events
overrides:
macros:
is_incremental: true
given:
- input: this
rows:
# Le maximum actuel est le 17 jan, un rattrapage de 3 jours place le seuil au 14 jan
- {event_id: 10, event_timestamp: "2024-01-17 12:00:00", event_data: "latest"}
- input: ref('base__events')
rows:
# Cet enregistrement est exactement à la limite — devrait être capturé
- {event_id: 11, event_timestamp: "2024-01-14 00:00:01", event_data: "boundary_record"}
# Cet enregistrement est une seconde avant la limite — le comportement dépend de votre filtre
- {event_id: 12, event_timestamp: "2024-01-13 23:59:59", event_data: "outside_window"}
expect:
rows:
- {event_id: 10, event_timestamp: "2024-01-17 12:00:00", event_data: "latest"}
- {event_id: 11, event_timestamp: "2024-01-14 00:00:01", event_data: "boundary_record"}
# event_id: 12 ne devrait PAS apparaître si votre filtre utilise >= (et non >)

Que l’enregistrement à la limite soit inclus ou exclu dépend de votre opérateur de filtre (>= vs >). Le test rend ce comportement explicite et évite que des modifications accidentelles de la logique passent inaperçues.

Tester la déduplication dans la fenêtre

Quand une fenêtre de rattrapage retraite des enregistrements déjà présents dans la table cible, le modèle doit les mettre à jour plutôt que les dupliquer. Testez ceci explicitement :

unit_tests:
- name: test_reprocessed_records_update_not_duplicate
model: fct__events
overrides:
macros:
is_incremental: true
given:
- input: this
rows:
- {event_id: 1, event_data: "stale_value", processed_at: "2024-01-16 10:00:00"}
- input: ref('base__events')
rows:
# Même event_id, valeur plus récente — devrait mettre à jour, pas dupliquer
- {event_id: 1, event_data: "fresh_value", processed_at: "2024-01-17 08:00:00"}
expect:
rows:
- {event_id: 1, event_data: "fresh_value"}
# Exactement une ligne — pas de doublons

Ce test vérifie que votre unique_key et votre logique de déduplication (QUALIFY/ROW_NUMBER) fonctionnent correctement ensemble lorsque la fenêtre de rattrapage provoque le retraitement d’enregistrements déjà traités.

Validation en production avec audit_helper

Les tests unitaires vérifient que la logique de votre modèle est correcte. La validation en production détecte si votre modèle en cours d’exécution s’écarte réellement de la vérité source — ce qui peut se produire lorsque les fenêtres de rattrapage sont mal dimensionnées ou lorsque des données arrivent en dehors de la fenêtre.

Le pattern standard : comparer une tranche récente de votre modèle incrémental avec un full-refresh de la même période.

{{ audit_helper.compare_queries(
a_query="SELECT * FROM {{ ref('fct__events') }} WHERE event_date >= CURRENT_DATE - 7",
b_query="""
SELECT *
FROM {{ ref('base__events') }}
WHERE event_date >= CURRENT_DATE - 7
QUALIFY ROW_NUMBER() OVER (PARTITION BY event_id ORDER BY updated_at DESC) = 1
""",
primary_key='event_id'
) }}

Cette comparaison retourne les lignes présentes dans une requête mais pas dans l’autre, et les lignes où la clé existe dans les deux mais dont les valeurs diffèrent. Des résultats non vides indiquent une dérive.

Exécuter cette comparaison chaque semaine (ou après tout problème de pipeline connu) vous donne une alerte précoce avant que la dérive ne s’accumule. Une fenêtre de recherche de 7 jours dans cette comparaison est intentionnelle — elle est plus large que la fenêtre de rattrapage typique de 3 jours, afin de pouvoir détecter les cas où des enregistrements tardifs ont manqué la fenêtre de justesse.

Interpréter les résultats de comparaison

compare_queries de audit_helper retourne un ensemble de résultats avec des colonnes indiquant si chaque ligne est in_a_only, in_b_only, ou in_both_or_identical. Les catégories ont des significations différentes :

  • in_a_only (présent dans l’incrémental, absent du full-refresh) : généralement sans danger — enregistrements dans votre modèle incrémental qui tombent en dehors de la fenêtre de comparaison. Peut aussi indiquer des enregistrements supplémentaires issus d’un bug de déduplication.
  • in_b_only (présent dans le full-refresh, absent de l’incrémental) : ce sont les cas dangereux. Enregistrements que votre modèle incrémental devrait avoir mais n’a pas. Ce sont des données tardives qui sont tombées en dehors de votre fenêtre de rattrapage.
  • in_both_different : les enregistrements existent dans les deux mais avec des valeurs différentes. Suggère soit qu’une mise à jour a été manquée, soit que la logique de déduplication a sélectionné une version différente.

Un nombre élevé d’enregistrements in_b_only dans votre fenêtre de comparaison (mais en dehors de votre fenêtre de rattrapage) est un signal direct que votre fenêtre est trop étroite.

Quand les tests unitaires ne suffisent pas

Les tests unitaires vérifient la logique en isolation avec des données de fixture contrôlées. Ils ne peuvent pas détecter :

  • Les problèmes de dimensionnement de fenêtre : si votre fenêtre de rattrapage est de 3 jours mais que 10 % des enregistrements arrivent dans les 5 jours, les tests unitaires avec des fixtures synthétiques ne captureront pas le taux de manqués réel.
  • Les changements de distribution des données sources : le profil de latence de votre système source peut évoluer dans le temps. Ce qui fonctionnait avec 3 jours l’an dernier pourrait nécessiter 7 jours cette année.
  • Les effets d’interaction à l’échelle : une déduplication qui fonctionne correctement sur 10 lignes de test peut avoir des cas limites qui n’apparaissent qu’avec des millions de lignes et une distribution réaliste de doublons.

La comparaison audit_helper est ce qui détecte ces problèmes. Combinez les tests unitaires pour la correction de la logique avec la comparaison en production périodique pour la détection de dérive. Aucun des deux n’est suffisant seul — l’un teste le code, l’autre teste le résultat.