Quand les migrations de base de données échouent en production : trois scénarios qui vous feront passer des nuits blanches

Vous venez d'exécuter une migration en production. Elle a réussi. Pas d'erreur, pas de timeout, pas de table verrouillée. Vous soufflez et passez à la suite.

Deux heures plus tard, votre téléphone sonne. Un rapport est cassé. Les données semblent erronées. Un service que vous aviez oublié écrit des valeurs NULL dans des tables critiques. La migration a réussi, mais votre système de production est en train de s'effondrer.

C'est le cauchemar des migrations de base de données. Contrairement aux déploiements d'applications où les échecs sont généralement immédiats et évidents, les échecs de migration peuvent se cacher pendant des heures ou des jours. Le temps que vous les remarquiez, les dégâts sont déjà faits.

Laissez-moi vous montrer trois scénarios réels où les migrations tournent mal, non pas parce que le SQL a échoué, mais parce que les effets secondaires ont pris tout le monde au dépourvu.

Scénario un : la nouvelle colonne qui a tout cassé

Votre équipe doit ajouter une colonne phone_number à la table users. La migration se déroule parfaitement en staging. Tous les tests passent. Vous poussez en production en toute confiance.

La colonne est créée. Aucune erreur. Mais quelques secondes plus tard, l'application commence à se comporter étrangement.

Voici ce qui s'est passé : l'application de production n'a pas encore été mise à jour. L'ancien code est toujours en cours d'exécution et envoie des requêtes comme SELECT * FROM users. Cela fonctionne - la nouvelle colonne est simplement ignorée. Le vrai problème est ailleurs. Un autre morceau de code commence à insérer des données dans phone_number, mais il utilise un format différent de celui attendu par la nouvelle application. Les numéros de téléphone arrivent dans des formats mélangés - certains avec des indicatifs pays, d'autres sans, certains avec des tirets, d'autres sans.

Considérez la migration qui a déclenché ce scénario :

ALTER TABLE users ADD COLUMN phone_number VARCHAR(20);

Cela semble inoffensif. Mais sans contrainte NOT NULL ni valeur par défaut, la colonne accepte n'importe quel format. Pire, si la table est volumineuse, ce ALTER TABLE verrouille la table en écriture pendant toute l'opération. En production, ce verrou peut mettre en file d'attente des centaines de requêtes en quelques secondes. Le vrai danger n'est pas le SQL lui-même - c'est que le schéma a changé avant que chaque instance de l'application soit prête à le gérer.

Vous avez maintenant des données incohérentes dans une colonne dont plusieurs systèmes vont dépendre. Votre équipe fait face à un choix difficile : essayer de nettoyer les données existantes, ou précipiter la nouvelle version de l'application en production avant qu'elle ne soit prête.

Le problème central ici est le timing. Le schéma a changé avant que le code applicatif qui le comprend soit complètement déployé. Dans un système distribué, toutes les instances ne se mettent pas à jour au même instant. Pendant une courte fenêtre - parfois plus longue - l'ancien code interagit avec le nouveau schéma.

Scénario deux : le changement de type qui a cassé un rapport nocturne

Celui-ci est plus subtil. Votre équipe décide de changer la colonne price de integer à decimal. Bonne idée - les prix ont besoin de précision. La migration s'exécute parfaitement. Aucune erreur immédiate. L'application semble contente.

Mais il y a six mois, quelqu'un a écrit une requête de rapport qui traite price comme un entier. Cette requête n'est pas utilisée sur les pages principales. Elle s'exécute une fois par nuit pour un rapport financier. À 2 heures du matin, le rapport échoue complètement. Chaque requête qui compare les prix avec des valeurs entières génère maintenant des erreurs de non-concordance de type.

C'est ce que les ingénieurs appellent un changement bloquant. Le changement de schéma n'a rien cassé de visible pendant la journée, mais il a silencieusement cassé un processus batch critique qui s'exécute la nuit. Au matin, l'équipe financière demande pourquoi les chiffres de la veille ne correspondent pas.

La partie dangereuse ? Vous pourriez ne découvrir cet échec que des heures plus tard. Et la solution n'est pas simple. Vous ne pouvez pas simplement annuler le changement de type sans une autre migration, ce qui comporte ses propres risques.

Scénario trois : la colonne supprimée qui a empoisonné trois tables

C'est le scénario le plus dangereux. Votre équipe est convaincue que la colonne old_status n'est plus utilisée. Elle a été dépréciée il y a des mois. Personne n'y fait référence dans l'application principale. Vous écrivez une migration pour la supprimer.

La migration se déroule sans accroc. La colonne disparaît. Aucune erreur nulle part.

Mais il y a un service d'arrière-plan - un job de synchronisation de données écrit par une équipe qui a quitté l'entreprise il y a deux ans - qui lit encore old_status périodiquement. Il ne plante pas lorsque la colonne est manquante. Il commence simplement à écrire des valeurs NULL dans d'autres tables. Les NULL se propagent. L'intégrité des données se brise silencieusement sur trois tables différentes au cours des deux heures suivantes.

Le temps que quelqu'un remarque, les dégâts sont faits. Vous ne pouvez pas simplement "annuler" la suppression de la colonne. Les données dans ces autres tables sont déjà corrompues. La récupération nécessite de comprendre exactement quelles lignes ont été affectées, de reconstruire les valeurs manquantes à partir des sauvegardes, et d'exécuter des scripts de réparation minutieux.

Pourquoi les migrations de base de données sont différentes des déploiements d'applications

Ces trois scénarios partagent un modèle commun : la migration s'est exécutée avec succès, mais les effets secondaires sont apparus plus tard. C'est ce qui rend les migrations de base de données fondamentalement différentes des déploiements d'applications.

Les trois scénarios ci-dessus partagent un modèle clair : un changement de schéma réussi déclenche un échec différé. Le diagramme suivant cartographie chaque scénario de la cause racine à la conséquence.

flowchart TD subgraph Scenario1[Scénario 1 : Nouvelle colonne] A1[Ajout colonne phone_number] --> B1[Ancien code écrit des formats incohérents] B1 --> C1[Corruption des données dans la nouvelle colonne] end subgraph Scenario2[Scénario 2 : Changement de type] A2[Changement price de integer à decimal] --> B2[Le rapport nocturne utilise une comparaison d'entiers] B2 --> C2[Échec du rapport à 2h du matin] end subgraph Scenario3[Scénario 3 : Colonne supprimée] A3[Suppression colonne old_status] --> B3[Service d'arrière-plan écrit des NULL] B3 --> C3[Les NULL se propagent sur 3 tables] C3 --> D3[Intégrité des données compromise] end

Lorsqu'un déploiement d'application échoue, vous le savez généralement immédiatement. Des erreurs apparaissent dans les logs. Les utilisateurs signalent des problèmes. Les alertes de monitoring se déclenchent. Vous pouvez revenir à la version précédente de l'application et rétablir le service rapidement.

Les migrations de base de données ne fonctionnent pas de cette façon. Un changement de schéma peut :

  • Créer des incohérences qui n'apparaissent que lorsque de nouvelles données arrivent
  • Casser des requêtes qui s'exécutent selon un planning, pas en continu
  • Provoquer une corruption des données qui se propage lentement dans les tables associées
  • Affecter des services que vous aviez oubliés ou dont vous ignoriez l'existence

Le pire ? Une fois les dégâts faits, vous ne pouvez pas simplement "annuler" un changement de schéma comme vous annulez une modification de code. Une colonne supprimée ne peut pas être restaurée facilement, surtout si d'autres tables dépendent déjà de son absence. Un type de données modifié nécessite une migration inverse qui comporte ses propres risques.

Une checklist pratique avant votre prochaine migration en production

Avant d'exécuter votre prochaine migration en production, passez en revue ces vérifications :

  • Identifiez tous les consommateurs. Listez chaque service, tâche cron, rapport et pipeline de données qui touche à la table concernée. Ne supposez pas que vous les connaissez tous.
  • Vérifiez l'exécution différée. Trouvez les requêtes qui s'exécutent selon un planning, les processus batch ou les jobs d'arrière-plan. Ce sont ceux qui échoueront silencieusement des heures plus tard.
  • Vérifiez la rétrocompatibilité. L'ancien code applicatif peut-il toujours fonctionner avec le nouveau schéma ? Pendant au moins un cycle de déploiement, votre schéma doit supporter à la fois l'ancien et le nouveau code.
  • Préparez un plan de récupération. Sachez exactement comment vous restaurerez les données si quelque chose tourne mal. Testez le processus de récupération, pas seulement la migration.
  • Exécutez la migration pendant les heures creuses. Même avec toutes les précautions, donnez-vous une marge pour détecter les problèmes avant qu'ils n'affectent les utilisateurs.

L'essentiel à retenir

Une migration réussie n'est pas celle qui s'exécute sans erreur. Une migration réussie est celle qui ne casse rien - maintenant, dans une heure, ou à 3 heures du matin quand le rapport nocturne s'exécute. Traitez chaque changement de schéma comme une bombe à retardement potentielle, et vérifiez que tous les systèmes, pas seulement les plus évidents, peuvent gérer la nouvelle structure avant de considérer la migration comme terminée.