Idempotence et backfill

Résumé

Un traitement est idempotent quand l’exécuter une ou plusieurs fois donne le même résultat. C’est la propriété qui rend sûrs les relances automatiques après une panne et les backfills (rejouer le passé pour corriger ou combler des données). La méthode générale : chaque exécution traite une tranche de données bien délimitée, lit les mêmes entrées à chaque fois et remplace son résultat au lieu de l’ajouter. Sans cela, une relance duplique des lignes ou produit des chiffres différents.

Prérequis

Notions de table, de partition et de transaction (ACID vs BASE). Les exemples SQL sont écrits pour BigQuery.

Le problème : une relance qui duplique

Un job charge les 1 000 commandes du jour dans une table. Il tombe en panne après en avoir inséré 600, puis l’orchestrateur le relance.

Stratégie d’écritureAprès la relanceVerdict
INSERT des 1 000 lignes600 anciennes + 1 000 nouvelles = 1 600 lignes, dont 600 en doubleNon idempotent
Remplacer la partition du jour1 000 lignesIdempotent
MERGE sur la clé de la commande1 000 lignesIdempotent
flowchart TD
    R["Relance après panne"] --> A{"Écriture"}
    A -- "INSERT" --> D["600 lignes en double"]
    A -- "remplacement de partition<br/>ou MERGE sur clé" --> OK["Même résultat qu'une exécution réussie"]

Les règles

Une tranche est la portion de données qu’une exécution traite (souvent un jour) ; Airflow l’appelle intervalle de données, et data_interval_start en est le début.

La documentation d’Airflow, un orchestrateur très utilisé, formule des consignes qui valent au-delà d’Airflow :1

  • Traiter une tâche comme une transaction. Ne jamais laisser de résultat incomplet, par exemple un fichier à moitié écrit dans un object storage.
  • Une tâche relancée doit produire le même résultat. Remplacer un INSERT par un UPSERT (insérer ou mettre à jour selon la clé).
  • Lire et écrire dans une partition précise. Ne jamais lire « les dernières données disponibles » : elles peuvent changer entre deux exécutions. Airflow suggère d’utiliser le début de l’intervalle de données (data_interval_start) comme partition, et de suivre le même découpage à l’écriture.
  • Ne pas dépendre de l’heure de l’exécution. Airflow précise que now() ne devrait jamais servir dans une tâche, surtout pour le calcul critique (c’est acceptable pour un journal temporaire) : il change à chaque relance, alors que l’intervalle de données est fixé par la planification.

Patrons d’écriture idempotents

1. Remplacer la partition de la tranche

Supprimer puis recharger la tranche, dans une seule transaction pour que personne ne voie l’état intermédiaire.

BEGIN TRANSACTION;
 
DELETE FROM sales.orders
WHERE order_date = DATE '2026-10-15';
 
INSERT INTO sales.orders (order_id, order_date, amount, status)
SELECT order_id, order_date, amount, status
FROM staging.orders_extract
WHERE order_date = DATE '2026-10-15';
 
COMMIT TRANSACTION;

2. MERGE sur une clé métier

MERGE sales.orders AS t
USING staging.orders_extract AS s
ON t.order_id = s.order_id
WHEN MATCHED THEN UPDATE SET amount = s.amount, status = s.status
WHEN NOT MATCHED THEN INSERT (order_id, order_date, amount, status)
  VALUES (s.order_id, s.order_date, s.amount, s.status);

Conditions d’idempotence : la clé doit identifier une commande de façon unique, et la source ne doit contenir qu’une ligne par clé (sinon BigQuery refuse le MERGE : dédupliquer avant). Ce MERGE ne gère pas les suppressions. Sur une grande table partitionnée, ajouter AND t.order_date = s.order_date à la condition ON permet à BigQuery de ne lire que les partitions concernées.

Aucune base n'impose l'unicité à votre place

Sur BigQuery, une clé primaire déclarée n’est pas appliquée, et Iceberg n’impose pas non plus d’unicité. C’est le MERGE sur une clé métier, ou le remplacement de la partition, qui garantit l’absence de doublons. Voir Maintenance d’un lakehouse.

3. En dbt

Un modèle incremental avec unique_key utilise une stratégie merge ; avec insert_overwrite, il remplace les partitions touchées. Dans les deux cas, la fenêtre de données relues doit être choisie avec soin : voir dbt Incremental Models vs Snowflake Dynamic Tables. Sur un lac de fichiers sans format de table, seul le remplacement de partition est possible : dbt et le lakehouse.

Backfill

Un backfill rejoue un traitement sur une période passée : un bug corrigé, une source qui livre en retard, une nouvelle colonne à reconstruire.

Un seul chemin de code

Le backfill doit emprunter exactement le même code que l’exécution normale, appliqué à une tranche passée. Deux chemins (un pour le quotidien, un pour le rejeu) finissent par diverger.

Précautions

RisqueExempleParade
AnachronismeEnrichir un événement de 2021 avec l’adresse email actuelle de l’utilisateur donne un résultat faux pour 2021Joindre l’état de la dimension à la date de l’événement (historique de type SCD 2, voir Slowly Changing Dimensions, types 0 à 3)

Le risque d’anachronisme est le plus fréquent : une jointure sur l’état courant d’une dimension fausse tout résultat historique.

Vérifier qu’un traitement est idempotent

Le test le plus simple : l’exécuter deux fois de suite sur la même tranche et comparer.

SELECT COUNT(*) AS n, BIT_XOR(FARM_FINGERPRINT(TO_JSON_STRING(t))) AS h
FROM sales.orders AS t
WHERE order_date = DATE '2026-10-15';

Le nombre de lignes et l’empreinte (un XOR des empreintes de chaque ligne, donc indépendant de l’ordre ; deux lignes strictement identiques s’annulent, c’est COUNT(*) qui détecte un doublon) doivent être identiques avant et après la seconde exécution. À automatiser dans les tests du pipeline.

Notes liées

Notes de bas de page

  1. Airflow, Best Practices, section « Creating a task », documentation consultée le 24 octobre 2026 (version 3.3.2). ↩