Changements de schéma sans interruption avec le modèle extension/contraction
Planifiez et livrez des changements de schéma sans interruption avec le modèle extension/contraction, des remplissages sûrs, des versions compatibles, des vérifications et un retour en arrière.

Pourquoi les changements de schéma provoquent des interruptions
Les changements de schéma provoquent des interruptions lorsque les versions de l’application, les workers en arrière-plan et la base de données ne s’accordent plus sur les structures et les valeurs valides. La panne peut être évidente, par exemple chaque requête renvoie une erreur, ou progressive, avec une hausse de la latence des requêtes, des écritures en échec, du retard sur les répliques et une file de tâches à rejouer.
Un déploiement en production modifie rarement tous les processus en même temps. Les déploiements progressifs laissent fonctionner côte à côte des instances anciennes et nouvelles de l’application. Des workers de longue durée peuvent conserver une ancienne version pendant des heures, des clients mobiles peuvent rester actifs pendant des mois, et des tâches de reporting ou d’intégration peuvent utiliser des tables sans passer par l’application principale. Tous partagent une même base de données.
Les échecs fréquents incluent :
- Le nouveau code écrit dans une colonne avant la fin de la migration qui la crée.
- L’ancien code lit une table ou une colonne qu’une version ultérieure a renommée ou supprimée.
- Une réécriture de table, un remplissage de données ou la création d’un index consomme assez d’E/S et de CPU pour ralentir le trafic normal.
- Une commande de schéma attend un verrou tandis que les requêtes s’accumulent derrière elle.
- Une nouvelle contrainte refuse les écritures d’un processus qui n’a pas encore été mis à jour.
Le risque vient souvent de l’acquisition du verrou, pas du temps d’exécution théorique. Un ALTER TABLE rapide peut attendre derrière une transaction longue. Pendant cette attente, les requêtes suivantes peuvent s’accumuler derrière le verrou de schéma en attente, et une petite migration devient alors un blocage à l’échelle de l’application.
L’absence d’interruption exige que chaque état intermédiaire de la base de données reste utilisable par toutes les versions de l’application encore susceptibles de s’exécuter. Ajoutez d’abord des structures compatibles, déplacez le trafic et les données par étapes maîtrisées, puis retirez l’ancien chemin seulement après la disparition de son dernier consommateur.
Cet effort se justifie pour les systèmes qui reçoivent du trafic en direct, utilisent des déploiements progressifs, ont des objectifs de disponibilité stricts ou des procédures de reprise coûteuses. Pour un petit outil interne avec une base peu sollicitée, une fenêtre de maintenance testée peut être préférable. La décision doit refléter le coût d’une interruption et la complexité opérationnelle de la migration.
Le modèle extension/contraction en clair
Le modèle extension/contraction transforme un changement incompatible en une succession de versions compatibles. Pendant un temps, la base de données prend en charge deux représentations pendant que le code et les données passent de l’ancienne à la nouvelle.
La séquence comporte trois parties :
- Étendez le schéma en ajoutant des colonnes, des tables, des index ou des contraintes sans supprimer ce dont le code actuel a besoin.
- Effectuez la transition en déployant du code compatible, en déplaçant les données historiques et en orientant les lectures et les écritures vers la nouvelle représentation.
- Contractez le schéma en supprimant l’ancien code et les anciens objets de base de données après avoir vérifié qu’ils ne sont plus utilisés.
Supposons qu’une table PostgreSQL stocke le nom complet d’une personne dans full_name et que l’application ait besoin de champs distincts first_name et last_name. La phase d’extension ajoute des colonnes nullables tout en conservant full_name. Une version compatible écrit les représentations nécessaires pendant la transition. Un remplissage sépare les valeurs existantes selon une règle explicite pour les noms impossibles à découper de manière fiable. Les lectures ne basculent qu’une fois les nouveaux champs suffisamment remplis. La phase de contraction supprimera ensuite full_name.
Cet ordre convient aux déploiements progressifs, car l’ancienne version trouve toujours full_name et la nouvelle trouve les trois colonnes. Il préserve aussi la possibilité de revenir à la version précédente de l’application. Si la nouvelle version se comporte mal, l’ancienne peut fonctionner car ses dépendances de schéma n’ont pas été supprimées.
Revenir en arrière dans la base de données diffère du retour à une version antérieure de l’application. Inverser une migration après avoir transformé les données peut supprimer des informations ou restaurer une valeur obsolète. Pendant la transition, préférez rediriger le trafic de l’application vers la représentation connue, tout en laissant les objets ajoutés dans la base. Corrigez la migration vers l’avant une fois l’incident stabilisé.
Ce modèle ne signifie pas que chaque changement exige une logique de double écriture. Ajouter une colonne facultative utilisée seulement par le nouveau code peut demander une migration additive et un déploiement. Les renommages, les changements de représentation, les séparations de tables et les modifications de champs obligatoires nécessitent généralement davantage de phases, car deux versions de l’application ne peuvent pas autrement partager le schéma en toute sécurité.
Classez le changement avant de choisir les étapes
Le plan de migration doit correspondre aux risques réels de verrouillage, de réécriture, de compatibilité et de conversion des données. Traiter chaque ALTER TABLE comme une opération équivalente produit soit des précautions inutiles, soit une version dangereuse.
Les changements additifs sont généralement les plus simples. Une colonne nullable, une table séparée ou un index créé avec une méthode en ligne peuvent souvent être introduits avant que le code de l’application les utilise. La commande exige tout de même un verrou, testez donc son comportement avec une table et une charge de transactions proches de la production.
Les changements destructifs comprennent la suppression ou le renommage de colonnes, le resserrement de types, le remplacement de tables et l’ajout de contraintes plus strictes. Ils invalident une hypothèse du code existant. Placez-les dans la phase de contraction, après avoir retiré les références du code et les consommateurs externes.
Les opérations qui modifient les données méritent une évaluation distincte. Convertir des horodatages, normaliser des numéros de téléphone, fusionner des enregistrements ou découper du texte libre peut faire perdre des informations. Définissez le traitement des valeurs invalides et ambiguës avant de lancer le remplissage. Si une transformation est irréversible, conservez la source jusqu’à ce que le résultat ait passé les contrôles métier.
Une revue préalable utile couvre cinq questions :
- Quel verrou chaque instruction demande-t-elle, et combien de temps peut-elle attendre ou conserver ce verrou ?
- L’opération va-t-elle réécrire la table, générer beaucoup de WAL ou augmenter le retard des répliques ?
- Quelles applications, tâches, rapports et quels consommateurs de capture des changements de données utilisent les objets concernés ?
- Les versions actuelles et proposées peuvent-elles fonctionner avec chaque état de transition ?
- Quel signal suspend l’opération et quel état exact reste-t-il après son arrêt ?
Exécutez la migration exacte sur des données dont le volume et la distribution sont réalistes. Une table de test avec mille lignes propres en dit peu sur une table de production qui contient des centaines de millions de lignes, des tuples larges, des lignes mortes, des valeurs très déséquilibrées et des transactions longues.
Étendre le schéma en toute sécurité dans PostgreSQL
Une extension PostgreSQL sûre s’appuie sur de courts changements de métadonnées, des attentes de verrou limitées et des opérations en ligne séparées lorsque la base l’exige. Ajoutez la nouvelle structure avant de déployer du code qui en dépend.
Ajouter une colonne nullable sans valeur par défaut est généralement une courte opération de métadonnées :
BEGIN;
SET LOCAL lock_timeout = '2s';
SET LOCAL statement_timeout = '30s';
ALTER TABLE customers
ADD COLUMN phone_e164 text;
COMMIT;
Le délai d’attente évite que la version attende indéfiniment derrière une transaction ouverte. Si le verrou ne peut pas être acquis rapidement, laissez la migration échouer, examinez ce qui bloque et réessayez à un moment moins risqué. N’effectuez pas de nouvelles tentatives automatiques dans une boucle serrée, car des demandes de verrou répétées peuvent continuer à perturber le trafic de production.
Les versions récentes de PostgreSQL peuvent ajouter une colonne avec une valeur par défaut constante sans écrire immédiatement cette valeur dans chaque ligne existante. Cette optimisation ne rend pas toutes les valeurs par défaut inoffensives. Une expression volatile peut exiger une réécriture, et ALTER TABLE nécessite toujours un bref verrou ACCESS EXCLUSIVE. Confirmez le comportement de la version PostgreSQL déployée et de l’expression exacte au lieu de vous fier à une règle générale.
Un CREATE INDEX classique peut bloquer les écritures. Utilisez la création concurrente lorsque la table doit rester accessible en écriture :
CREATE INDEX CONCURRENTLY idx_customers_phone_e164
ON customers (phone_e164);
CREATE INDEX CONCURRENTLY ne peut pas s’exécuter dans un bloc de transaction. L’opération est plus longue, demande davantage de travail et peut attendre des transactions plus anciennes, mais les insertions, mises à jour et suppressions ordinaires peuvent continuer. Elle consomme tout de même du CPU, des E/S et du WAL. Surveillez donc la latence de la base et les répliques pendant son exécution.
Une création concurrente en échec peut laisser un index invalide. Examinez l’état de l’index avant de réessayer, puis supprimez ou reconstruisez délibérément l’objet invalide. Les outils de migration qui enveloppent chaque fichier dans une transaction doivent proposer un mode non transactionnel pris en charge pour les opérations d’index concurrentes.
Les nouvelles tables sont souvent plus faciles à introduire que les transformations sur place. Pour une relation un-à-plusieurs ou plusieurs-à-plusieurs, ajoutez la table cible et ses index tout en conservant la colonne source. Différez la suppression de la source jusqu’à ce que les nouvelles écritures, les données historiques, les lectures et les consommateurs en aval aient été déplacés.
Les changements de type demandent une attention particulière. Certains ne modifient que les métadonnées, d’autres réécrivent chaque ligne ou acquièrent trop longtemps un verrou restrictif. Pour une conversion risquée, ajoutez une colonne du type cible, remplissez-la par lots, basculez les accès de l’application, puis supprimez l’original plus tard. L’équipe peut aussi y consigner les échecs de conversion au lieu de faire réussir ou échouer un gros ALTER COLUMN TYPE d’un seul bloc.
Déployez un code qui reste compatible
Un code d’application compatible accepte les valeurs de transition manquantes et n’exige jamais une migration destructive au cours du même déploiement. L’extension de la base doit être terminée avant que la première instance de l’application commence à utiliser le nouvel objet.
La double écriture est utile lorsque les deux représentations doivent rester à jour. Effectuez les deux écritures dans la même transaction de base de données lorsque c’est possible. Une seconde écriture asynchrone peut échouer après la réussite de la première et créer une divergence que les lectures révéleront ensuite.
La logique de double écriture doit aussi avoir une seule source d’autorité. Si phone_e164 est dérivé de phone, définissez quelle entrée l’emporte quand les deux sont fournies et appliquez la même normalisation dans les gestionnaires d’API, les workers, les imports et les outils d’administration. Sinon, deux chemins de code apparemment corrects peuvent enregistrer des résultats différents.
Les lectures doivent basculer après les écritures. Gardez les lectures sur le champ établi pendant que les nouvelles écritures remplissent les deux formes et que le remplissage traite les lignes historiques. Après vérification, déployez un chemin de lecture qui privilégie le nouveau champ et n’utilise l’ancienne valeur que selon une règle de repli définie. Mesurez l’usage du repli. Un repli silencieux peut masquer à jamais des données incomplètes.
Une séquence de versions typique est la suivante :
- La version 1 ajoute les nouveaux objets de base de données sans modifier le comportement de l’application.
- La version 2 écrit les représentations de transition tout en gardant les lectures établies.
- La version 3 bascule les lectures après le remplissage et la réussite des contrôles de cohérence.
- La version 4 cesse de maintenir l’ancienne représentation après l’expiration des critères de retour en arrière.
- La version 5 supprime les anciennes références de code, puis la base est nettoyée plus tard.
Gardez les contrats d’API publics séparés des changements de schéma physique. Renommer une colonne de base de données ne demande pas de renommer immédiatement un champ dans les réponses web, mobiles ou d’intégration. Faites évoluer ces contrats selon leur propre politique de compatibilité, surtout lorsque les clients ne peuvent pas être mis à jour avec le serveur.
Recensez chaque écrivain. Les gestionnaires HTTP ne sont qu’une source de changements parmi d’autres. Les consommateurs de files, les tâches planifiées, les scripts d’import, les outils de réparation de données, les déclencheurs de base et les opérations administratives directes peuvent continuer à produire des lignes sous l’ancien format. Lorsque cela est pertinent, étiquetez les connexions à la base avec le nom de l’application et journalisez l’usage des chemins de transition afin de rendre visible tout processus oublié.
Les processus de longue durée peuvent conserver des hypothèses périmées dans des requêtes préparées, des métadonnées en cache ou une couche de mapping objet-relationnel. Testez les redémarrages progressifs et le comportement des pools de connexions avant la contraction. Un processus qui n’a pas reçu de trafic récemment peut tout de même échouer à la première exécution d’une tâche rare.
Remplissez les données sans surcharger la base
Un remplissage sûr met à jour de petits lots qui peuvent reprendre après interruption et ralentit lorsque la santé de la production se dégrade. Il commence seulement une fois que les écrivains en direct peuvent maintenir la nouvelle représentation.
Choisissez les lots selon leur durée et leur impact sur la base, pas selon un nombre universel de lignes. Mille lignes étroites peuvent se terminer en quelques millisecondes, alors que mille lignes contenant de grosses valeurs ou des transformations coûteuses peuvent générer beaucoup d’E/S. Commencez prudemment et visez des transactions qui se terminent en quelques secondes. Validez entre les lots afin que les verrous et les anciennes versions de lignes ne s’accumulent pas dans une seule transaction.
PostgreSQL ne prend pas directement en charge ORDER BY et LIMIT dans un simple UPDATE. Sélectionnez un lot dans une expression de table commune, puis mettez ces lignes à jour :
WITH batch AS (
SELECT id
FROM my_table
WHERE id > $1
AND new_col IS NULL
ORDER BY id
LIMIT 1000
)
UPDATE my_table AS target
SET new_col = transform_expression(target.old_col)
FROM batch
WHERE target.id = batch.id
AND target.new_col IS NULL
RETURNING target.id;
L’application enregistre le plus grand id terminé comme curseur. La mise à jour conditionnelle rend les nouvelles exécutions idempotentes. Une panne après validation ne corrompt donc pas les lignes déjà traitées. Enregistrez la progression avec assez de précaution pour que le curseur ne puisse pas avancer au-delà d’un lot non validé.
Un curseur id croissant évite de balayer plusieurs fois le début de la table, mais il ne détecte pas les corrections tardives ou les lignes insérées avant le curseur. Terminez par un passage de rattrapage sur toutes les valeurs NULL restantes. Si les identifiants ne sont pas ordonnés ou si les lignes peuvent changer d’état d’éligibilité, utilisez une table de travail ou un autre point de contrôle explicite au lieu de supposer qu’un seul parcours vers l’avant suffit.
Plusieurs workers peuvent réclamer des lignes avec FOR UPDATE SKIP LOCKED, mais le parallélisme augmente la pression d’écriture et complique le suivi de progression. Ne combinez pas des lignes ignorées avec un curseur qui avance définitivement au-delà d’elles. Une file d’identifiants réclamés ou un balayage répété des lignes éligibles est plus sûr pour des workers parallèles.
Régulez la tâche à partir de mesures de production telles que la latence des requêtes, les connexions actives, les attentes de verrou, la génération de WAL, le retard de relecture des répliques et l’augmentation des lignes mortes. Suspendez-la lorsqu’un seuil est franchi, puis reprenez depuis le point de contrôle. Des pauses fixes sont simples, mais les retours de la base s’adaptent mieux aux changements de trafic.
Évitez de modifier chaque ligne si seules certaines nécessitent un traitement. Filtrez sur le nouveau champ, l’état source ou un marqueur de migration. Si la transformation est coûteuse, calculez-la hors de la transaction de mise à jour lorsque la cohérence le permet, puis effectuez une courte écriture conditionnelle. Conservez un nombre et un échantillon des valeurs refusées au lieu d’inventer silencieusement des données.
Autovacuum et les répliques doivent absorber le travail après chaque mise à jour. Un remplissage peut réussir sur le primaire alors que les répliques prennent beaucoup de retard ou que le gonflement de la table dégrade les requêtes ultérieures. Les limites de débit doivent tenir compte de ce coût différé, pas seulement du temps d’exécution immédiat du lot.
Vérifiez les données et le trafic de production
Une migration est prête pour la contraction seulement lorsque les contrôles de données, la télémétrie de l’application et les preuves liées aux dépendances confirment que le nouveau chemin fait autorité. Un compteur de tâches terminées ne prouve pas à lui seul que le résultat est correct.
Commencez par l’exhaustivité et la cohérence. L’opérateur PostgreSQL IS DISTINCT FROM compare les valeurs tout en traitant explicitement NULL, contrairement à <>, qui produit un résultat inconnu si l’un des deux côtés vaut NULL :
SELECT count(*)
FROM customers
WHERE normalize_phone(phone) IS DISTINCT FROM phone_e164;
N’exécutez pas à répétition un comptage complet non indexé sur une grande table très sollicitée. Utilisez une validation ponctuelle et maîtrisée, des plages d’identifiants limitées, des échantillons ou un processus de vérification temporaire qui progresse dans la table. La bonne méthode dépend du coût d’une erreur et de la marge de capacité disponible dans la base.
La vérification doit couvrir :
- Aucune valeur manquante imprévue ne subsiste dans les lignes qui exigent le nouveau champ.
- La nouvelle valeur correspond à la transformation convenue, y compris pour les entrées malformées et vides.
- Les nouvelles lignes et les mises à jour restent cohérentes après la fin du traitement historique.
- L’usage du repli de lecture a atteint le seuil prévu, généralement zéro pour le trafic contrôlé par le serveur.
- Les taux d’erreur, la latence des requêtes, les verrous et le retard des répliques restent dans les limites de la version.
Comparez aussi les résultats métier, pas seulement les colonnes. Si une migration modifie des prix, des autorisations, l’état de comptes ou des identifiants, validez les totaux et les invariants dont dépendent les utilisateurs. Deux colonnes peuvent correspondre mécaniquement tout en encodant toutes deux une règle métier erronée.
Observez un cycle d’exploitation complet avant le nettoyage. La bonne durée dépend du comportement réel du système, pas d’une règle fixe d’une semaine. Elle peut devoir couvrir le traitement de fin de mois, une tâche de facturation rare, des nouvelles tentatives tardives dans une file ou la durée de vie maximale d’un ancien client mobile. Consignez les preuves du déplacement de chaque consommateur.
Effectuez un déploiement canari du changement de lecture lorsque l’architecture de l’application le permet. Envoyez une petite part du trafic vers le nouveau chemin de lecture, comparez les résultats, puis étendez progressivement. Gardez une action de retour en arrière simple : rediriger les lectures vers la représentation établie sans annuler le remplissage.
Ajoutez les contraintes lorsque les données sont prêtes
Les contraintes ne doivent devenir strictes qu’après la mise en conformité de tous les écrivains et la validation des données existantes. Appliquer NOT NULL, une contrainte de contrôle ou une clé étrangère pendant l’extension peut bloquer le trafic ou refuser les écritures d’un ancien processus.
PostgreSQL peut ajouter une contrainte de contrôle avec NOT VALID, ce qui applique la règle aux lignes nouvelles ou modifiées sans analyser immédiatement toutes les lignes historiques. Validez-la séparément après le remplissage :
ALTER TABLE customers
ADD CONSTRAINT customers_phone_e164_present
CHECK (phone_e164 IS NOT NULL) NOT VALID;
ALTER TABLE customers
VALIDATE CONSTRAINT customers_phone_e164_present;
Une fois la validation réussie, les versions PostgreSQL qui le prennent en charge peuvent utiliser cette preuve lors du passage de la colonne à NOT NULL, évitant une nouvelle analyse complète de la table. La dernière modification exige toujours un verrou fort sur la table. Utilisez donc un délai d’attente de verrou limité et un plan de nouvelle tentative :
ALTER TABLE customers
ALTER COLUMN phone_e164 SET NOT NULL;
ALTER TABLE customers
DROP CONSTRAINT customers_phone_e164_present;
La contrainte de contrôle temporaire peut rester si elle apporte une valeur, mais conserver des contraintes équivalentes encombre le catalogue sans modifier la règle.
Les clés étrangères peuvent suivre une séquence semblable avec NOT VALID et VALIDATE CONSTRAINT. Les nouvelles écritures sont vérifiées après la création de la contrainte, tandis que la validation historique a lieu plus tard. Ajoutez délibérément l’index de prise en charge lorsque le comportement de suppression ou de mise à jour de la relation référencée risquerait sinon de provoquer des analyses coûteuses.
La validation par l’application doit précéder l’application de la règle par la base, mais ne la remplace pas. Le code produit des erreurs plus claires pour l’utilisateur, tandis que la base protège les données écrites par tous les chemins. Pendant le déploiement, surveillez les violations de contrainte afin d’identifier un écrivain oublié par l’audit des dépendances.
Contractez l’ancien chemin en toute sécurité
La phase de contraction doit supprimer les dépendances de l’application avant de supprimer les objets de base de données. Une fois que la télémétrie et les vérifications montrent que le nouveau chemin fait autorité, le nettoyage peut avancer via des versions distinctes.
Commencez par cesser de lire l’ancien champ et retirez la logique de repli. Désactivez ensuite ses écritures et observez assez longtemps la production pour détecter les chemins rares. Supprimez les indicateurs de fonctionnalité, déclencheurs, vues de compatibilité, scripts de réparation et tâches planifiées qui mentionnent l’ancienne représentation. Recherchez dans le code source exporté et les scripts de migration, mais examinez aussi les rapports, les requêtes d’intégration et les configurations de capture des changements de données hors du dépôt principal.
Un ordre de nettoyage sûr est le suivant :
- Supprimez les lectures de repli et confirmez qu’elles n’apparaissent plus dans la télémétrie.
- Arrêtez les anciennes écritures et supprimez le code de synchronisation.
- Retirez les références de l’application de toutes les versions déployables.
- Supprimez les index et contraintes obsolètes avec la méthode en ligne appropriée.
- Supprimez l’ancienne colonne ou table dans une version de base ultérieure.
Supprimer une colonne PostgreSQL modifie principalement le catalogue, mais exige tout de même un verrou ACCESS EXCLUSIVE. Une instruction courte peut donc attendre derrière une transaction longue et bloquer le travail suivant. Appliquez un délai d’attente de verrou, examinez au préalable les transactions longues et programmez la tentative pendant une période moins risquée.
Utilisez DROP INDEX CONCURRENTLY pour un index obsolète lorsque bloquer les écritures serait inacceptable. Comme la création concurrente, cette commande ne peut pas s’exécuter dans un bloc de transaction et comporte des restrictions que l’outil de migration doit gérer.
Ne combinez pas le nettoyage du code et la suppression physique dans une même version. Cette séparation permet à l’application nettoyée de fonctionner avec une base qui contient toujours l’objet inutilisé. En cas de problème dans l’application, le retour en arrière reste possible sans recréer le schéma ni reconstruire les données.
Avant de supprimer une table, vérifiez la propriété des séquences, vues, fonctions, droits, déclencheurs, publications de réplication et requêtes externes. Évitez d’utiliser CASCADE comme raccourci dans une migration de production, car la commande peut supprimer des dépendances qui ne faisaient pas partie du changement prévu.
Gérez le retour en arrière et les étapes en échec
La planification du retour en arrière doit définir une action sûre pour chaque phase au lieu de s’appuyer sur une seule migration descendante générique. Les objets additifs, le déplacement de données, les bascules de lecture et les suppressions ont des propriétés de récupération différentes.
Si l’extension n’arrive pas à obtenir son verrou, laissez l’application inchangée et réessayez après avoir résolu la transaction bloquante. Si la création d’index concurrente échoue, examinez si elle a laissé un index invalide, puis nettoyez cet objet précis avant une autre tentative.
Si un remplissage génère de la charge, suspendez-le. Les lots idempotents déjà validés peuvent rester en place. Réduisez la taille ou la cadence des lots, traitez la transformation coûteuse et reprenez au point de contrôle. Annuler des millions de mises à jour correctes augmente généralement le risque sans aider la production à se rétablir.
Si un nouveau chemin de lecture renvoie des résultats incorrects, redirigez les lectures vers l’ancienne représentation tout en conservant les nouvelles données pour le diagnostic. Continuez la double écriture seulement si elle est reconnue correcte. Si l’écrivain lui-même est défectueux, désactivez-le ou revenez à la version antérieure de l’application avant de réparer les lignes affectées.
Après la contraction, la récupération peut exiger de restaurer des données plutôt que de simplement déployer une ancienne version. Définissez explicitement le point de non-retour. Effectuez la sauvegarde ou l’instantané requis par la politique de récupération du système, testez la restauration avant la version et conservez l’ancien objet pendant la durée de rétention convenue lorsque le coût de stockage le permet.
Les commandes de schéma peuvent être transactionnelles, mais les effets externes ne sont pas toujours couverts. Les opérations d’index concurrentes, les messages de file, les changements de cache et les déploiements d’application ne partagent pas une même transaction atomique. Le guide d’exploitation doit décrire l’état observable après chaque échec partiel et la commande qui permet de reprendre en toute sécurité.
Évitez les pièges fréquents des migrations
La plupart des migrations sans interruption qui échouent appliquent le nouvel état trop tôt ou oublient un consommateur de l’ancien état. Les pièges suivants méritent une revue explicite avant approbation.
- Ajouter
NOT NULLalors qu’une ancienne instance de l’application peut encore omettre le champ. - Exécuter un gros remplissage dans une seule transaction, en conservant trop longtemps les verrous et les versions de lignes.
- Renommer une colonne comme s’il s’agissait d’un ajout, alors que l’ancien code utilise encore son nom d’origine.
- Basculer les lectures avant que tous les chemins d’écriture et les lignes historiques aient rempli la nouvelle représentation.
- Considérer un déploiement réussi comme la preuve que les rapports, workers, répliques et intégrations sont compatibles.
Un autre échec subtil vient de la synchronisation bidirectionnelle. Un déclencheur copie old_col vers new_col, tandis que le code de l’application copie new_col vers old_col. Des différences de normalisation ou d’ordre d’exécution des déclencheurs peuvent créer des boucles, écraser des valeurs intentionnelles ou rendre l’autorité floue. Préférez une seule direction et documentez quelle représentation fait autorité pendant chaque version.
Les valeurs par défaut peuvent masquer les mises à jour manquantes des écrivains. Si une nouvelle colonne obligatoire reçoit une valeur par défaut vide ou générique, l’ancien code semble compatible tout en stockant des données sémantiquement invalides. Utilisez une transition nullable lorsque l’absence contient une information utile pour le diagnostic, puis appliquez la vraie règle lorsque chaque écrivain fournit une valeur significative.
Un indicateur de fonctionnalité ne rend pas à lui seul une commande de schéma incompatible sûre. Un chemin de code désactivé peut encore être chargé, préparé ou exécuté par un ancien processus. L’objet de base doit rester présent jusqu’à ce qu’aucune version déployable ou active ne le référence.
La responsabilité de la migration compte aussi. Confiez la transition jusqu’à la contraction à une personne ou une équipe, y compris les dates de vérification et de suppression. Sinon, les colonnes temporaires, indicateurs et tâches de synchronisation peuvent rester des mois, ce qui augmente le coût de chaque changement ultérieur.
Remplacez une colonne de téléphone sans interruption
Remplacer customers.phone par customers.phone_e164 exige une colonne additive, une politique de conversion définie, du code compatible, un remplissage limité, une bascule de lecture et un nettoyage différé. La politique de conversion doit précéder le SQL, car toutes les valeurs stockées ne peuvent pas être normalisées automatiquement.
Commencez par classer les valeurs existantes. Les numéros valides peuvent être convertis si le contexte de pays requis est connu. Les valeurs vides peuvent devenir NULL. Les numéros ambigus ou malformés doivent figurer dans un rapport d’exception au lieu d’être devinés. Déterminez si le produit exige que chaque client ait un numéro de téléphone, car cela indiquera plus tard si NOT NULL convient.
Ajoutez la colonne avec un délai d’attente de verrou court :
BEGIN;
SET LOCAL lock_timeout = '2s';
ALTER TABLE customers
ADD COLUMN phone_e164 text;
COMMIT;
Déployez du code qui normalise les nouvelles entrées et écrit phone et phone_e164 dans une même transaction. Gardez d’abord les lectures sur phone. Mettez à jour chaque écrivain, y compris les imports de comptes, les outils d’assistance, les tâches de workers et les tests qui créent des données de démonstration client.
Remplissez les lignes éligibles lors de transactions courtes. Enregistrez le dernier identifiant traité, le nombre de conversions, le nombre de lignes ignorées et la raison de chaque catégorie d’échec. Limitez le débit de la tâche en fonction de la latence de production et du retard des répliques. Une fois le parcours vers l’avant terminé, recherchez de nouveau les valeurs NULL éligibles afin de détecter les insertions simultanées ou les lignes manquées après un redémarrage.
Exécutez des contrôles de cohérence avec les mêmes règles de normalisation que l’application, puis échantillonnez manuellement les préfixes internationaux, les extensions, les valeurs vides, les doublons de contacts et les anciennes données importées. Un nombre de lignes prouve la couverture, pas la validité d’un numéro de téléphone.
Déployez un chemin de lecture qui renvoie phone_e164 lorsqu’il est présent et n’utilise phone que pour une exception journalisée. Surveillez l’usage du repli et les erreurs de normalisation. Résolvez les exceptions restantes plutôt que de laisser le repli devenir un comportement permanent.
Lorsque le nouveau champ fait autorité, supprimez le repli et cessez d’écrire dans phone. Observez les tâches rares et le trafic des intégrations pendant un cycle d’exploitation approprié. Ajoutez la contrainte validée seulement si la règle du produit l’exige.
Enfin, supprimez les références de code à phone. Supprimez séparément ses index ou contraintes, puis la colonne dans une migration ultérieure avec une attente de verrou limitée. Si la bascule de lecture échoue à tout moment avant cette suppression, revenez au comportement antérieur de l’application tant que les deux colonnes restent disponibles.
Cet exemple révèle aussi une difficulté métier que les mécanismes de schéma ne résolvent pas : découper ou normaliser des données saisies par des personnes n’est pas toujours sans perte. Le plan de migration doit préserver les exceptions et donner à un responsable les moyens de les traiter.
Vérifiez chaque version avant sa mise en production
Une liste de contrôle de version doit prouver la compatibilité, limiter l’impact en production et indiquer l’action de récupération pour la phase en cours. Conservez les preuves avec le changement pour qu’un opérateur n’ait pas à reconstituer l’intention pendant un incident.
Avant le déploiement, confirmez les points suivants :
- La version de l’application fonctionne avec l’état de la base avant et après cette version.
- Des délais d’attente de verrou et d’instruction sont définis pour les commandes de schéma qui pourraient attendre derrière le trafic.
- La tâche de remplissage ou de validation possède des contrôles de progression, de suspension, de reprise et de limitation du débit.
- Les tableaux de bord couvrent les erreurs, la latence, les verrous, la charge de base, le WAL et le retard des répliques.
- L’action de retour en arrière a été testée sans dépendre d’un objet déjà supprimé.
Consignez des conditions de fin explicites. Par exemple : aucune nouvelle erreur de cohérence pendant un cycle complet de tâche, aucune lecture de repli issue du trafic contrôlé par le serveur, tous les consommateurs connus mis à jour et une requête de validation maîtrisée réussie. Le pourcentage d’avancement est utile pendant un remplissage, mais 100 % des lignes traitées ne signifie pas 100 % de résultat correct.
Examinez l’ordre de la migration indépendamment de la revue de code. Un ensemble correct de SQL et de changements d’application peut tout de même échouer si le déploiement les exécute dans le mauvais ordre. Indiquez quelle étape ne peut commencer qu’après l’achèvement d’une autre.
Les conditions d’arrêt doivent être chiffrées lorsque c’est possible. Définissez une latence de requête, une attente de verrou, un retard de réplique, un taux d’erreur et une durée de lot acceptables. Lorsqu’un seuil est franchi, l’opérateur doit savoir s’il faut suspendre une tâche, annuler une instruction en attente ou rediriger les lectures, sans demander une nouvelle approbation pendant l’incident.
La migration n’est terminée que lorsque la nouvelle représentation prend en charge les lectures et les écritures, que les données historiques ont passé les vérifications, que l’ancien objet a été supprimé et que les mécanismes opérationnels temporaires ont disparu.
Rendez le processus reproductible
Un guide de migration réutilisable fait de l’extension/contraction une activité normale de mise en production, avec des responsables désignés et des jalons mesurables. Il doit être assez court pour être suivi pendant un déploiement en direct et assez précis pour décrire les états d’échec partiel.
Utilisez cinq sections dans le guide :
- Extension : opérations de schéma exactes, verrous attendus, délais d’attente et exigences de transaction.
- Compatibilité : code concerné, écrivains, lecteurs, indicateurs, clients et ordre de déploiement.
- Remplissage : politique de transformation, traitement par lots, points de contrôle, régulation et gestion des exceptions.
- Vérification : contrôles SQL, invariants métier, télémétrie et seuils d’achèvement.
- Contraction : suppression des dépendances, période d’observation, nettoyage physique et limites de récupération.
Attribuez un responsable et une date de fin prévue à chaque objet de transition. Suivez les colonnes, index, indicateurs, déclencheurs et tâches au même endroit. Le nettoyage fait partie de la migration, ce n’est pas de la maintenance facultative.
Pour les équipes qui créent avec Koder.ai, le mode Planification peut aider à détailler ces phases et points de contrôle avant de commencer les changements en production. L’export du code source permet aussi au SQL de migration et à la logique de compatibilité de recevoir la même revue que le reste du code de l’application. Koder.ai prend en charge le déploiement, l’hébergement, les instantanés et le retour en arrière, mais il ne faut pas supposer qu’un retour à une version antérieure de l’application inverse une transformation de données déjà validée. Préservez la compatibilité du schéma tant que le plan de récupération de la base dépend de l’ancienne représentation.
Programmez les travaux qui génèrent beaucoup d’écritures durant les périodes de faible trafic lorsque c’est possible, mais ne faites pas du calendrier votre seul contrôle de sécurité. Des transactions limitées, une régulation fondée sur les retours de la base, une progression observable et une action de suspension testée permettent de garder une migration en ligne maîtrisable lorsque le trafic ou les données se comportent autrement que prévu.
FAQ
Pourquoi un changement de schéma peut-il provoquer une interruption de service ?
Les changements de schéma provoquent des incidents lorsque les anciennes et les nouvelles versions de l’application attendent des structures de base de données différentes. Lors d’un déploiement progressif, les deux versions peuvent fonctionner en même temps. Supprimer ou renommer une colonne trop tôt peut donc faire échouer des lectures ou des écritures.
Qu’est-ce que le modèle de migration extension/contraction ?
Le modèle extension/contraction découpe un changement incompatible en étapes sûres. Ajoutez d’abord la nouvelle structure, déplacez ensuite le code et les données vers elle, puis supprimez l’ancienne structure quand plus aucun consommateur ne l’utilise.
Comment renommer ou remplacer une colonne de base de données sans interruption ?
Ajoutez d’abord la nouvelle colonne et conservez l’ancienne. Déployez un code capable de fonctionner avec les deux champs, remplissez les lignes existantes par petits lots, basculez les lectures après validation, puis supprimez l’ancienne colonne dans une version ultérieure.
Puis-je ajouter une colonne PostgreSQL sans bloquer le trafic ?
En général, oui. Dans PostgreSQL, ajouter une colonne nullable sans valeur par défaut est souvent une courte opération de métadonnées, mais elle exige tout de même un verrou sur la table. Définissez un délai d’attente de verrou court pour que la migration échoue plutôt que d’attendre derrière une longue transaction.
Comment créer un index sans bloquer les écritures ?
Utilisez CREATE INDEX CONCURRENTLY lorsque la table doit rester accessible en écriture. L’opération prend plus de temps et charge davantage la base de données. Elle ne peut pas s’exécuter dans un bloc de transaction, surveillez donc la latence, le WAL et le retard des répliques pendant son exécution.
Quand une application doit-elle écrire à la fois dans les anciens et les nouveaux champs ?
Écrivez les deux valeurs dans la même transaction de base de données lorsque les deux représentations doivent rester à jour. Définissez quel champ l’emporte en cas de désaccord et appliquez les mêmes règles de normalisation dans les API, les workers, les imports et les outils d’assistance.
Comment remplir en toute sécurité une grande table PostgreSQL ?
Traitez des lots courts qui peuvent reprendre après interruption, puis validez après chaque lot. Enregistrez un point de contrôle, ne mettez à jour que les lignes qui en ont encore besoin, et ralentissez ou suspendez la tâche lorsque la latence des requêtes, les attentes de verrou, le volume de WAL ou le retard des répliques augmente.
Comment savoir si un remplissage de données est terminé et correct ?
Ne basculez pas les lectures simplement parce que le remplissage est terminé. Vérifiez que les valeurs requises existent, comparez les anciennes et les nouvelles représentations, surveillez les lectures de repli et confirmez que les nouvelles écritures restent cohérentes après le traitement des données historiques.
Quand ajouter NOT NULL, des contraintes CHECK ou des clés étrangères ?
Ajoutez des contraintes strictes après avoir validé les données existantes et vérifié que chaque écrivain actif fournit des valeurs valides. PostgreSQL permet d’ajouter certaines contraintes avec NOT VALID, de les appliquer aux nouvelles lignes et de valider les lignes historiques séparément.
Quand peut-on supprimer sans risque l’ancien chemin de schéma ?
Supprimez d’abord les lectures de repli, cessez ensuite les anciennes écritures et observez le système pendant un cycle d’exploitation complet. Quand plus aucune application, tâche, rapport, intégration ou client ne référence l’ancien objet, supprimez le code concerné puis la colonne ou la table lors d’une version ultérieure.