Quand les anciennes données rencontrent un nouveau schéma : rétro-remplissage et vérification des enregistrements historiques

Vous venez d'ajouter une nouvelle colonne à une table de base de données qui tourne en production depuis trois ans. L'application écrit désormais à la fois dans l'ancienne et la nouvelle structure. Mais qu'en est-il des millions de lignes créées avant ce changement ? Elles restent avec des valeurs NULL dans la nouvelle colonne, contenant des données techniquement incomplètes.

C'est le moment où de nombreuses équipes réalisent que la migration de schéma ne se limite pas à ajouter des colonnes. Il s'agit de s'assurer que chaque enregistrement existant se met à jour avec les nouvelles règles. Le processus de remplissage des anciennes données dans une nouvelle structure s'appelle le rétro-remplissage (backfill). Et si vous le faites sans précaution, vous pouvez verrouiller votre table de production pendant des heures, ralentir votre application et frustrer tous ceux qui attendent le retour des requêtes.

Pourquoi le rétro-remplissage n'est pas un simple UPDATE

L'approche naïve consiste à exécuter une seule grosse requête UPDATE qui définit la nouvelle colonne pour chaque ligne où elle est NULL. Si votre table contient quelques milliers de lignes, cela peut se terminer en quelques secondes. Mais si vous avez affaire à des dizaines de millions de lignes, cette seule requête peut maintenir un verrou de table pendant longtemps. Les autres requêtes s'empilent. L'application ralentit. Les utilisateurs commencent à voir des timeouts.

Le rétro-remplissage doit être planifié avec le même soin que toute opération de production. La première étape consiste à définir exactement quelles données doivent être remplies. Supposons que vous ayez ajouté une colonne status dont la valeur est dérivée de deux colonnes existantes : is_active et deleted_at. Votre logique de rétro-remplissage doit correspondre aux mêmes règles que celles utilisées par le nouveau code de l'application lors de l'écriture de nouveaux enregistrements. Si l'application définit status sur 'active' lorsque is_active est vrai et que deleted_at est nul, votre rétro-remplissage doit faire de même.

Traitement par lots : la méthode sécurisée

Au lieu d'une requête massive, traitez les données par petits lots. Prenez mille ou dix mille lignes qui ont encore des valeurs NULL dans la nouvelle colonne, mettez-les à jour, puis faites une brève pause avant de passer au lot suivant. Cela donne à la base de données le temps de respirer et permet aux autres requêtes de se dérouler sans attendre.

Certaines bases de données prennent en charge nativement les mises à jour par lots. PostgreSQL, par exemple, permet d'utiliser UPDATE ... WHERE ... LIMIT avec une sous-requête. MySQL ne supporte pas directement LIMIT dans UPDATE, mais vous pouvez utiliser une boucle dans une procédure stockée ou gérer le lotissement depuis votre script de migration. L'essentiel est de garder chaque lot suffisamment petit pour que la durée de verrouillage reste acceptable pour votre charge de travail.

Voici un modèle général qui fonctionne sur de nombreuses bases de données :

tant qu'il y a des lignes avec NULL dans new_column :
    sélectionner un lot de clés primaires où new_column IS NULL
    mettre à jour ces lignes avec la valeur calculée
    attendre un court intervalle

L'intervalle d'attente ne vise pas à être lent. Il s'agit de donner une chance aux autres opérations de s'exécuter. Même 100 millisecondes entre les lots peuvent faire une grande différence dans le comportement de la base de données sous charge.

Le diagramme suivant illustre cette approche itérative sécurisée :

flowchart TD A[Début] --> B{Des lignes avec NULL ?} B -- Oui --> C[Sélectionner un lot de 1000 PK où new_column IS NULL] C --> D[Calculer la valeur de la nouvelle colonne pour le lot] D --> E[Mettre à jour les lignes WHERE id IN (lot)] E --> F[Pause 100ms] F --> B B -- Non --> G[Terminé]

Pour un exemple concret, supposons que vous ayez ajouté une colonne status dérivée de is_active et deleted_at. Une mise à jour par lots dans PostgreSQL pourrait ressembler à ceci :

UPDATE your_table
SET status = CASE
    WHEN is_active = true AND deleted_at IS NULL THEN 'active'
    WHEN is_active = false AND deleted_at IS NULL THEN 'inactive'
    ELSE 'deleted'
END
WHERE id IN (
    SELECT id
    FROM your_table
    WHERE status IS NULL
    LIMIT 1000
);

La sous-requête interne sélectionne un petit lot de clés primaires où la nouvelle colonne est encore NULL. La mise à jour externe ne touche que ces lignes, ce qui maintient le verrou court. Répétez cette opération en boucle jusqu'à ce qu'il n'y ait plus de NULL.

Deux sources de vérité pendant le rétro-remplissage

Pendant que le rétro-remplissage s'exécute, l'application continue d'écrire de nouvelles données à la fois dans l'ancienne et la nouvelle structure. Cela signifie que vous avez maintenant deux sources de vérité pour les mêmes données logiques :

  • Données écrites par l'application (toujours cohérentes car l'application écrit aux deux endroits simultanément)
  • Données remplies par le processus de rétro-remplissage (doivent correspondre exactement à la logique de l'application)

Les deux doivent produire les mêmes valeurs pour la même entrée. Si votre logique de rétro-remplissage contient un bug, ou si la logique de l'application a changé entre le début et la fin du rétro-remplissage, vous vous retrouverez avec des données incohérentes. C'est pourquoi la vérification n'est pas optionnelle.

Vérification : plus que la simple recherche de NULL

Après la fin du rétro-remplissage, l'instinct naturel est d'effectuer une vérification rapide : « Reste-t-il des valeurs NULL ? » Cela vous indique que la colonne est remplie, mais cela ne vous dit pas si les valeurs sont correctes.

Une vérification appropriée signifie comparer la valeur calculée avec la valeur attendue pour chaque ligne. Vous lisez les anciennes colonnes, calculez ce que la nouvelle valeur devrait être, et la comparez avec ce qui est réellement stocké dans la nouvelle colonne. Toute divergence nécessite une investigation.

Une divergence peut provenir de plusieurs sources :

  • Un bug dans la logique de rétro-remplissage qui calcule la valeur différemment de l'application
  • Des données qui ont changé entre le moment où le rétro-remplissage les a traitées et le moment où la vérification s'exécute
  • L'application qui écrit une valeur différente parce que sa logique a été mise à jour en cours de migration
  • Des conditions de concurrence où une ligne a été mise à jour par l'application après le rétro-remplissage mais avant la vérification

Chaque divergence doit être retracée jusqu'à sa cause racine. Parfois, c'est une correction simple. Parfois, cela révèle un problème plus profond dans le plan de migration.

Vérification progressive

Vous n'avez pas besoin de vérifier l'intégralité de la table en une seule passe. Commencez par un échantillon aléatoire. Prenez mille lignes, comparez les valeurs et voyez si tout correspond. Si l'échantillon est conforme, passez à un échantillon plus grand. Si celui-ci est conforme, effectuez une vérification complète.

Pour les grandes tables, une vérification complète ne signifie pas lire chaque ligne une par une. Vous pouvez écrire une requête qui compte les divergences sans récupérer toutes les données. Quelque chose comme :

SELECT COUNT(*) FROM table
WHERE computed_value != actual_new_value

Si le compte est nul, tout est propre. S'il n'est pas nul, vous savez combien de lignes nécessitent une attention particulière, et vous pouvez les examiner par lots.

Quand la vérification réussit

Une fois que la vérification confirme que chaque ligne de la nouvelle structure correspond à la valeur attendue, vous avez la certitude que les données sont cohérentes. Mais la migration n'est pas encore terminée. L'application écrit toujours dans les deux structures. La prochaine étape consiste à arrêter la lecture de l'ancien schéma et à se fier entièrement au nouveau. Cette transition est une phase distincte avec ses propres risques et étapes de vérification.

Le rétro-remplissage et la vérification sont les fondations qui rendent cette transition sûre. Sans eux, vous passez à un nouveau schéma avec des données incomplètes ou incorrectes. Avec eux, vous savez exactement ce que vous avez.

Liste de contrôle pratique

  • Définir la logique exacte pour calculer la nouvelle valeur, en correspondance avec le code de l'application
  • Choisir une taille de lot qui maintient la durée de verrouillage acceptable pour votre base de données
  • Implémenter le lotissement avec une pause entre les lots
  • Exécuter le rétro-remplissage dans une fenêtre de maintenance ou une période de faible trafic
  • Vérifier avec un échantillon aléatoire avant la vérification complète
  • Investiguer chaque divergence avant de continuer
  • Documenter la logique de rétro-remplissage et les résultats de vérification pour référence future

L'essentiel à retenir

Le rétro-remplissage n'est pas une corvée technique à expédier. C'est l'étape où vous prouvez que votre logique de migration fonctionne sur des données réelles à grande échelle. Si vous sautez la vérification, vous faites confiance à votre code de rétro-remplissage comme s'il était parfait et qu'aucune donnée n'a changé pendant le processus. Cette confiance est rarement justifiée. Exécutez le rétro-remplissage par lots, vérifiez les résultats systématiquement, et ne passez à la phase suivante que lorsque vous avez la preuve que chaque enregistrement est correct.