Renommer des colonnes, fractionner des tables et modifier des contraintes sans temps d'arrêt

Vous avez une table users avec une colonne full_name. L'équipe décide qu'elle doit s'appeler display_name. Si vous la renommez directement, chaque application qui lit encore full_name plantera au moment où le changement arrive en production. La colonne a disparu, les requêtes échouent, et les utilisateurs voient des erreurs.

Ce n'est pas un problème hypothétique. Les équipes renomment des colonnes, fractionnent des tables en deux et modifient des contraintes à chaque sprint. L'approche naïve — modifier le schéma et corriger le code plus tard — provoque des incidents de production qui auraient pu être évités. La solution est un pattern appelé expand-contract, et il fonctionne pour les trois scénarios.

L'idée centrale : ajouter d'abord, basculer progressivement, supprimer en dernier

Le pattern expand-contract comporte trois phases. D'abord, vous étendez le schéma en ajoutant de nouvelles structures à côté des anciennes. Ensuite, vous migrez les applications et les données pour utiliser les nouvelles structures. Enfin, vous contractez en supprimant les anciennes structures une fois que plus rien n'en dépend.

Le diagramme ci-dessous illustre le pattern expand-contract en trois phases et son application au renommage d'une colonne, au fractionnement d'une table et à la modification d'une contrainte.

flowchart TD A[Début] --> B[Phase 1 : Expand] B --> C[Ajouter nouvelle colonne/table/contrainte] C --> D[Écrire dans les deux structures ancienne et nouvelle] D --> E[Phase 2 : Migrer] E --> F[Remplir les données existantes] F --> G[Mettre à jour les applications pour lire depuis la nouvelle structure] G --> H[Tous les consommateurs migrés ?] H -- Non --> G H -- Oui --> I[Phase 3 : Contract] I --> J[Vérifier qu'aucune dépendance ne subsiste sur l'ancienne structure] J --> K[Supprimer l'ancienne colonne/table/contrainte] K --> L[Terminé]

L'idée clé est que vous ne faites jamais un changement cassant en une seule étape. Vous gardez toujours l'ancien chemin fonctionnel jusqu'à ce que le nouveau soit complètement adopté. Cela garantit zéro temps d'arrêt pendant les modifications de schéma, à condition de suivre la séquence correctement.

Renommer une colonne sans rien casser

Parcourons le renommage de full_name vers display_name. Dans la phase d'expansion, vous ajoutez une nouvelle colonne display_name à la table users. Vous ne supprimez pas full_name. La nouvelle version de votre application commence à écrire dans les deux colonnes. Chaque insertion ou mise à jour écrit la même valeur dans full_name pour les anciens consommateurs et dans display_name pour les nouveaux.

Voici les commandes SQL pour chaque phase du renommage :

-- Phase 1 : Expand - ajouter la nouvelle colonne
ALTER TABLE users ADD COLUMN display_name VARCHAR(255);

-- Exemple d'écriture double (logique applicative, pas seulement SQL)
-- Lors de l'insertion ou de la mise à jour d'un utilisateur, écrire dans les deux colonnes :
INSERT INTO users (full_name, display_name) VALUES ('Alice', 'Alice');
UPDATE users SET full_name = 'Bob', display_name = 'Bob' WHERE id = 42;

-- Phase 2 : Migrer - remplir les données existantes
UPDATE users SET display_name = full_name WHERE display_name IS NULL;

-- Phase 3 : Contract - supprimer l'ancienne colonne
ALTER TABLE users DROP COLUMN full_name;

Cette séquence garantit qu'à aucun moment la base de données ne rejette une requête ou ne perd de données.

Une fois la colonne créée et l'application écrivant dans les deux, vous exécutez un remplissage. C'est un processus par lots qui copie toutes les valeurs existantes de full_name vers display_name pour chaque ligne. Vous vérifiez que les comptages correspondent et vous contrôlez quelques enregistrements au hasard pour vous assurer que rien n'a été perdu.

Vient ensuite la bascule. Toutes les applications qui lisent les noms d'utilisateurs doivent être mises à jour pour lire depuis display_name au lieu de full_name. Cela peut se faire progressivement. Certains services basculent en premier, d'autres suivent. Pendant cette période, les deux colonnes restent alimentées, donc tout service qui lit encore full_name fonctionne toujours.

Une fois que chaque application et chaque requête ont été migrées, vous entrez dans la phase de contraction. Vous supprimez la colonne full_name. L'ensemble du processus prend des jours ou des semaines selon le nombre de services à mettre à jour, mais il n'y a jamais de moment où les utilisateurs ne peuvent pas accéder à l'application.

Fractionner une table en deux

Ce scénario est plus complexe. Imaginez que votre table orders stocke les détails de la commande et les informations de paiement dans la même ligne. L'équipe souhaite séparer les données de paiement dans une table dédiée payments. Vous ne pouvez pas simplement créer la nouvelle table et arrêter d'écrire dans l'ancienne, car les applications existantes lisent toujours depuis orders.

La phase d'expansion crée la table payments. La nouvelle version de l'application commence à écrire les données de paiement aux deux endroits. Chaque fois qu'une commande est créée ou mise à jour, l'application écrit les détails de paiement dans la table orders pour les anciens consommateurs et dans la table payments pour la nouvelle structure. C'est ce qu'on appelle l'écriture double, et c'est la partie la plus difficile à bien faire. Les deux écritures doivent réussir ou les deux doivent être annulées. Des écritures partielles corrompraient vos données.

Le remplissage est crucial ici. Vous devez copier toutes les données de paiement existantes de orders vers payments. Exécutez-le par lots pour éviter de verrouiller la table trop longtemps. Après chaque lot, vérifiez que le nombre d'enregistrements et les montants totaux des paiements correspondent entre les deux tables. S'ils ne correspondent pas, arrêtez-vous et enquêtez avant de continuer.

Une fois le remplissage vérifié et toutes les applications mises à jour pour lire les données de paiement depuis payments au lieu de orders, vous entrez dans la phase de contraction. Vous supprimez les colonnes de paiement de orders. Cette étape nécessite d'être certain qu'aucune requête, rapport ou service legacy n'accède encore à ces colonnes. Vérifiez vos logs de base de données, logs d'application et la surveillance des requêtes avant de supprimer quoi que ce soit.

Modifier une contrainte nullable en NOT NULL

Ce scénario semble simple mais prend souvent les équipes au dépourvu. Votre table users a une colonne email qui autorise les valeurs nulles. Le métier exige désormais que chaque utilisateur ait un email. Si vous modifiez la colonne en NOT NULL directement, la base de données rejettera le changement car les lignes existantes avec des emails nuls violent la contrainte.

La phase d'expansion ici n'ajoute pas une nouvelle colonne au sens traditionnel. Une approche courante consiste à ajouter une nouvelle colonne email_not_null qui reflète email mais avec une contrainte NOT NULL. La nouvelle version de l'application écrit dans les deux colonnes. Pour les insertions, les deux colonnes reçoivent la même valeur. Pour les mises à jour, les deux sont mises à jour.

Le remplissage est l'étape décisive. Chaque ligne avec un email nul doit être corrigée. Vous devez soit fournir une valeur par défaut, coordonner avec les utilisateurs pour qu'ils renseignent leur email, ou travailler avec d'autres équipes pour fournir les données manquantes. Ce n'est pas seulement un problème technique. C'est un problème de qualité de données et d'organisation. Le script de remplissage doit journaliser chaque ligne qu'il ne peut pas corriger et alerter l'équipe pour traiter ces cas manuellement.

Après que toutes les lignes ont des emails valides et que toutes les applications lisent depuis email_not_null, vous contractez en supprimant l'ancienne colonne email. Si vous souhaitez conserver le nom de colonne d'origine, vous pouvez renommer email_not_null en email une fois l'ancienne colonne supprimée.

Liste de contrôle pratique pour les modifications de schéma

Avant d'exécuter une migration de schéma en production, parcourez cette liste :

  • L'ancien schéma peut-il toujours répondre aux requêtes après le changement ?
  • Existe-t-il un chemin d'écriture double pour toutes les nouvelles données ?
  • Le script de remplissage a-t-il été testé sur une copie des données de production ?
  • Avez-vous vérifié que tous les consommateurs ont migré avant de supprimer quoi que ce soit ?
  • Avez-vous un plan de retour en arrière si la phase de contraction révèle une dépendance oubliée ?

Ce qu'il faut retenir

Chaque modification de schéma que vous effectuez en production doit suivre la même séquence : ajouter la nouvelle structure, migrer les données et les applications progressivement, et supprimer l'ancienne structure uniquement lorsque plus rien n'en dépend. Que vous renommiez une colonne, fractionniez une table ou resserriez une contrainte, le pattern est le même. Le coût est la patience et une coordination minutieuse. La récompense est zéro temps d'arrêt et zéro requête cassée.