Modifier des schémas de base de données sans casser la production

Vous avez une base de données qui tourne depuis cinq, dix ou quinze ans. Elle contient des millions de transactions, des milliers de tables et des centaines de procédures stockées écrites par des personnes qui ne travaillent peut-être plus dans l'entreprise. Chaque fois que quelqu'un doit ajouter une colonne, changer un type de données ou corriger un index, la même question revient : "Si cette migration échoue, combien de temps faudra-t-il pour récupérer ?"

Dans des organisations comme celle-ci, la base de données n'est pas qu'un simple endroit pour stocker des données. C'est le cœur opérationnel de l'entreprise. Si l'application tombe, les utilisateurs peuvent attendre. Si la base de données se corrompt, les données peuvent être perdues définitivement. C'est pourquoi les modifications de schéma et de données sont souvent considérées comme des activités à haut risque, programmées le samedi soir à 2 heures du matin, dans l'espoir que personne ne s'en aperçoive avant lundi matin.

Cette approche ne passe pas à l'échelle. Plus une équipe veut livrer des fonctionnalités rapidement, plus les modifications de base de données sont nécessaires. Si chaque changement doit attendre une fenêtre de maintenance hebdomadaire, l'équipe produit s'impatiente. Mais si les changements sont faits sans précaution, le risque de corruption des données devient réel.

La vraie question n'est pas "quel outil de migration est le meilleur". C'est : "Comment modifier un schéma de base de données sans interrompre le service, et comment revenir en arrière si quelque chose tourne mal ?"

Une migration sécurisée commence par de petites étapes

Le principe de base est simple : chaque changement doit être possible sans casser la connexion avec les applications en cours d'exécution, et doit être réversible sans perte de données. Cela signifie que les modifications de schéma doivent se faire en plusieurs petites étapes, pas en un seul grand bond.

Prenons l'ajout d'une nouvelle colonne. Dans une base de données legacy, vous ajoutez la colonne avec une valeur par défaut ou en la rendant nullable. Vous n'ajoutez pas de contraintes strictes tout de suite. Les anciennes instances de l'application continuent de fonctionner car elles ne lisent pas la nouvelle colonne. Les nouvelles instances de l'application commencent à y écrire. Une fois que toutes les instances ont été mises à jour et tournent de manière stable, vous ajoutez des contraintes comme NOT NULL ou des clés étrangères dans une migration séparée. Si quelque chose tourne mal en cours de route, revenir en arrière est aussi simple que d'ignorer la nouvelle colonne. Pas besoin de supprimer des tables ou de restaurer à partir d'une sauvegarde.

Le diagramme de séquence suivant illustre le processus sécurisé et progressif décrit ci-dessus :

sequenceDiagram participant OldApp as Application (ancienne version) participant DB as Base de données participant NewApp as Application (nouvelle version) Note over DB: Étape 1 : Ajouter une colonne nullable DB->>DB: ALTER TABLE ADD COLUMN nullable Note over OldApp,DB: Étape 2 : L'ancienne app continue sans impact OldApp->>DB: Lecture/écriture (ignore la nouvelle colonne) DB-->>OldApp: Réponse Note over DB,NewApp: Étape 3 : Déployer la nouvelle app qui utilise la colonne NewApp->>DB: Écriture dans la nouvelle colonne DB-->>NewApp: OK NewApp->>DB: Lecture de la nouvelle colonne DB-->>NewApp: Données Note over DB: Étape 4 : Ajouter les contraintes DB->>DB: ALTER TABLE ADD NOT NULL Note over OldApp: Étape 5 : Supprimer l'ancienne app OldApp-->>OldApp: Désaffectée

Le même modèle s'applique pour changer un type de données. Supposons qu'une colonne price soit actuellement de type INTEGER mais doive devenir DECIMAL. L'approche sécurisée : ajoutez une nouvelle colonne appelée price_decimal, peuplez-la avec les valeurs converties de l'ancienne colonne, laissez l'application lire depuis la nouvelle colonne tout en continuant d'écrire dans les deux, puis supprimez l'ancienne colonne une fois que tout est stable. Revenir en arrière signifie que l'application lit à nouveau depuis l'ancienne colonne, qui existe toujours.

L'exemple SQL suivant montre les scripts de migration avant et arrière pour ajouter une colonne en toute sécurité :

-- Migration avant 1 : ajouter la colonne nullable
ALTER TABLE products ADD COLUMN discount_rate DECIMAL(5,2) NULL;

-- Remplissage des données (à exécuter après que l'application écrit dans la nouvelle colonne)
UPDATE products SET discount_rate = 0.00 WHERE discount_rate IS NULL;

-- Migration avant 2 : ajouter la contrainte NOT NULL
ALTER TABLE products ALTER COLUMN discount_rate SET NOT NULL;

-- Script de rollback (inverse les deux étapes)
ALTER TABLE products ALTER COLUMN discount_rate DROP NOT NULL;
ALTER TABLE products DROP COLUMN discount_rate;

Les changements complexes nécessitent des exécutions parallèles

Pour des changements plus complexes comme diviser une table en deux ou fusionner plusieurs tables, la technique s'appelle l'exécution parallèle. L'application écrit à la fois dans les anciennes et nouvelles structures simultanément, tandis que les requêtes de lecture sont progressivement basculées. L'équipe peut comparer les résultats des deux structures pour s'assurer qu'il n'y a pas de différences de données. Si une anomalie apparaît, l'application peut revenir à l'ancienne structure sans perte de données.

Cette approche nécessite un codage minutieux côté application. L'application doit connaître les deux structures et gérer les écritures dans les deux. Elle a également besoin d'une logique pour décider de quelle structure lire. Ce n'est pas trivial, mais c'est bien plus sûr que de tenter une migration big-bang unique qui fonctionne parfaitement ou provoque un incident majeur.

La migration concerne les données, pas seulement le schéma

Une erreur courante est de traiter la migration de base de données comme une simple opération de schéma. Les données qui existent déjà doivent rester cohérentes après la migration. Chaque migration nécessite deux scripts : un script avant et un script de rollback. Le script de rollback n'est pas simplement l'inverse du script avant. Il doit ramener les données exactement dans le même état qu'avant la migration, y compris les données qui ont pu être modifiées par l'application pendant le processus de migration.

Par exemple, si une migration renomme une colonne et transforme ses valeurs, le script de rollback doit inverser à la fois le nom de la colonne et la transformation des valeurs. Si l'application a écrit de nouvelles données dans la colonne renommée pendant la fenêtre de migration, le script de rollback doit gérer ces données correctement, pas simplement les supprimer.

Où la migration s'intègre dans le pipeline

Dans un pipeline CI/CD, la migration de base de données doit être une étape distincte qui s'exécute indépendamment du déploiement de l'application. Le pipeline ne doit pas exécuter la migration en même temps que le nouveau code est déployé. Au lieu de cela, la migration s'exécute en premier. Une fois que la migration est confirmée comme réussie, la nouvelle version de l'application est déployée. Si la migration échoue, le pipeline s'arrête et l'équipe est notifiée avant que l'application ne soit affectée.

Cette séparation est cruciale. Si la migration et le déploiement se produisent ensemble et que quelque chose tourne mal, il est difficile de dire si le problème vient du changement de schéma ou du nouveau code. Les exécuter séquentiellement donne une responsabilité claire pour chaque échec.

Quand exiger une approbation manuelle

Les organisations qui fonctionnent depuis longtemps adoptent généralement une règle simple : les migrations qui modifient les données (pas seulement le schéma) nécessitent une approbation manuelle. Les migrations de schéma uniquement additives, comme l'ajout d'une colonne nullable, peuvent s'exécuter automatiquement. Ce n'est pas parce que l'automatisation n'est pas fiable. C'est parce que les modifications de données ont des conséquences plus difficiles à prévoir que les modifications structurelles.

Une nouvelle colonne nullable ne cassera rien. Mais une migration qui met à jour des millions de lignes, transforme des valeurs ou fusionne des tables peut introduire des bugs subtils qui n'apparaissent que dans des conditions de données spécifiques. Une revue humaine avant de telles migrations est un filet de sécurité, pas un goulot d'étranglement.

Liste de contrôle pratique pour des migrations de base de données sécurisées

  • Ajoutez d'abord les colonnes comme nullables ou avec des valeurs par défaut, puis ajoutez les contraintes plus tard.
  • Changez les types de données en ajoutant une nouvelle colonne, en la remplissant et en basculant les lectures progressivement.
  • Pour les restructurations complexes, exécutez les anciennes et nouvelles structures en parallèle et comparez les résultats.
  • Écrivez toujours un script de rollback qui restaure les données exactement dans leur état pré-migration.
  • Exécutez les migrations avant les déploiements d'application, pas en même temps.
  • Exigez une approbation manuelle pour les migrations qui modifient les données, autorisez les migrations de schéma additives à s'exécuter automatiquement.

L'essentiel à retenir

Les migrations de base de données ne doivent pas être terrifiantes. La clé est de décomposer chaque changement en petites étapes réversibles. Ajoutez avant de supprimer. Exécutez l'ancien et le nouveau côte à côte avant de basculer. Et ayez toujours un moyen de revenir en arrière qui ne repose pas sur une restauration à partir d'une sauvegarde. Lorsque vous traitez chaque migration comme une série d'étapes sécurisées et testables, vous éliminez la peur et faites des modifications de base de données une partie normale de votre processus de livraison.