11 dic 2025·8 min di lettura

Modifiche allo schema senza downtime: migrazioni additive che restano sicure

Impara a fare modifiche allo schema senza downtime con migrazioni additive, backfill sicuri e rollout a fasi che mantengono i client vecchi funzionanti durante i rilasci.

Modifiche allo schema senza downtime: migrazioni additive che restano sicure

Cosa significa davvero zero-downtime per le modifiche allo schema

Zero-downtime non significa che nulla cambi. Significa che gli utenti possono continuare a lavorare mentre aggiorni il database e l'app, senza errori o flussi di lavoro bloccati.

Downtime è qualsiasi momento in cui il sistema non si comporta normalmente. Può manifestarsi con errori 500, timeout delle API, schermate che si caricano ma mostrano valori vuoti o sbagliati, job in background che crashano, o un database che accetta letture ma blocca le scritture perché una migrazione lunga tiene lock.

Una modifica allo schema può rompere più del solo UI principale. Punti di fallimento comuni includono client API che si aspettano una forma di risposta vecchia, job in background che leggono o scrivono colonne specifiche, report che interrogano tabelle direttamente, integrazioni di terze parti e script amministrativi interni che “ieri funzionavano”.

Le app mobili vecchie e i client in cache sono un problema frequente perché non puoi aggiornarli istantaneamente. Alcuni utenti mantengono una versione dell'app per settimane. Altri hanno connettività intermittente e ritentano richieste vecchie. Anche i client web possono comportarsi come “versioni vecchie” quando un service worker, CDN o proxy cache mantiene codice o assunzioni obsolete.

L'obiettivo reale non è “una grande migrazione che finisce presto.” È una sequenza di piccoli passi in cui ogni passo funziona da solo, anche quando client diversi sono su versioni diverse.

Una definizione pratica: dovresti essere in grado di distribuire nuovo codice e nuovo schema in qualsiasi ordine, e il sistema funziona comunque.

Questo approccio ti aiuta ad evitare la trappola classica: distribuire una nuova app che si aspetta una colonna nuova prima che la colonna esista, o aggiungere una nuova colonna che il codice vecchio non sa gestire. Pianifica i cambiamenti per essere prima additivi, rilasciali a fasi e rimuovi i percorsi vecchi solo dopo esserti accertato che nessuno li usa.

Parti da modifiche additive che non rompono il codice esistente

La strada più sicura verso modifiche allo schema senza downtime è aggiungere, non sostituire. Aggiungere una colonna o una tabella nuova raramente rompe qualcosa perché il codice esistente può continuare a leggere e scrivere la forma vecchia.

Rinominare e cancellare sono mosse rischiose. Una rinomina è in pratica “aggiungi nuovo + rimuovi vecchio”, e la parte "rimuovi vecchio" è dove i client più vecchi vanno in crash. Se ti serve rinominare, trattalo come un cambiamento in due fasi: aggiungi prima il campo nuovo, mantieni quello vecchio per un po’, e rimuovilo solo quando sei sicuro che nessuno dipenda più da esso.

Quando aggiungi colonne, inizia con campi nullable. Una colonna nullable permette al codice vecchio di continuare a inserire righe senza conoscere il nuovo campo. Se alla fine vuoi NOT NULL, aggiungilo prima come nullable, esegui il backfill e poi imponi NOT NULL più tardi. I default possono aiutare, ma attenzione: aggiungere un default può comunque toccare molte righe in alcuni database, rallentando il cambiamento.

Gli indici sono un’altra aggiunta “sicura ma non gratuita”. Possono rendere le letture più veloci, ma creare e mantenere un indice può rallentare le scritture. Aggiungi indici quando sai esattamente quale query li userà e considera di farlo in orari più tranquilli se il DB è sotto carico.

Una semplice serie di regole per migrazioni additive:

  • Aggiungi nuove tabelle o colonne per prime, mantenendo intatte quelle vecchie.
  • Rendi i nuovi campi opzionali (nullable) finché i dati non sono popolati.
  • Mantieni funzionanti le query e i payload vecchi finché i client non si aggiornano.
  • Rimanda vincoli (NOT NULL, unique, foreign key) fino a dopo i backfill.

Piano di rollout passo-passo che mantiene i client vecchi funzionanti

Considera le modifiche zero-downtime come un rollout, non come un singolo deploy. L'obiettivo è lasciare che versioni vecchie e nuove dell'app girino insieme mentre il database si sposta gradualmente alla nuova forma.

Una sequenza pratica:

  1. Aggiungi il nuovo schema in modo compatibile. Crea nuove colonne o tabelle, consenti null e evita vincoli stretti che il codice vecchio non può soddisfare. Se ti serve un indice, aggiungilo in modo che non blocchi le scritture.
  2. Distribuisci le modifiche backend che parlano entrambe le “lingue”. Aggiorna l'API in modo che accetti richieste vecchie e nuove. Inizia a scrivere il campo nuovo mantenendo corretto quello vecchio. Questa fase di “dual write” è ciò che rende sicure le versioni miste dei client.
  3. Esegui il backfill dei dati esistenti a piccoli batch. Popola la colonna nuova per le righe vecchie gradualmente. Limita la dimensione dei batch, aggiungi pause se necessario e traccia il progresso in modo da poter mettere in pausa se il carico aumenta.
  4. Cambia le letture solo dopo che la copertura è alta. Quando la maggior parte delle righe è backfillata e hai fiducia, fai leggere il backend preferendo il campo nuovo. Mantieni un fallback al campo vecchio per un po'.
  5. Rimuovi il campo vecchio per ultimo, e solo quando è davvero inutilizzato. Aspetta che le build mobili vecchie siano per lo più scomparse, che i log non mostrino più letture del campo vecchio e che tu abbia un piano di rollback pulito. Poi rimuovi la colonna e il codice correlato.

Esempio: introduci full_name ma i client più vecchi inviano ancora first_name e last_name. Per un periodo il backend può costruire full_name in scrittura, backfillare gli utenti esistenti, poi leggere full_name per default continuando a supportare i payload vecchi. Solo quando l’adozione è chiara elimini i campi vecchi.

Backfill senza sorprese: come popolare i nuovi dati in sicurezza

Un backfill popola una nuova colonna o tabella per le righe esistenti. È spesso la parte più rischiosa delle modifiche zero-downtime perché può generare carico pesante sul database, lock lunghi e comportamenti “metà migrati” confusi.

Inizia scegliendo come eseguire il backfill. Per dataset piccoli, una runbook manuale una tantum può andare bene. Per dataset grandi, preferisci un worker in background o un task schedulato che possa girare ripetutamente e fermarsi in sicurezza.

Batti il lavoro a batch così controlli la pressione sul DB. Non aggiornare milioni di righe in una sola transazione. Punta a una dimensione dei chunk prevedibile e a una breve pausa tra i batch così il traffico utente normale resta fluido.

Un pattern pratico:

  • Seleziona un piccolo batch (per esempio le prossime 1.000 righe) usando una chiave indicizzata.
  • Aggiorna solo ciò che manca (evita di riscrivere righe già backfillate).
  • Commit veloce, poi dormi un attimo.
  • Registra il progresso (ultimo ID o timestamp processato).
  • Riprova in caso di errore senza ricominciare da capo.

Rendi il job riavviabile. Memorizza un semplice indicatore di progresso in una tabella dedicata e progetta il job in modo che rieseguirelo non corrompa i dati. Aggiornamenti idempotenti (per esempio, UPDATE ... WHERE new_field IS NULL) sono tuoi alleati.

Valida mentre procedi. Tieni traccia di quante righe sono ancora senza il nuovo valore e aggiungi alcuni controlli di sanità. Per esempio: nessun saldo negativo, timestamp in un intervallo atteso, status in un set consentito. Controlla a campione record reali.

Decidi cosa deve fare l'app mentre il backfill è incompleto. Un'opzione sicura è le letture fallback: se il campo nuovo è null, calcola o leggi il valore vecchio. Esempio: aggiungi preferred_language. Finché il backfill non termina, l'API può restituire la lingua esistente dalle impostazioni del profilo quando preferred_language è vuoto, e iniziare a richiedere il campo solo dopo il completamento.

Regole di compatibilità API per versioni client miste

Esercitati con migrazioni additive rapidamente
Modella uno schema sicuro e additivo e rigenera backend e API quando i requisiti cambiano.
Prova AppMaster

Quando spedisci una modifica allo schema, raramente controlli ogni client. Gli utenti web si aggiornano velocemente, mentre build mobili vecchie possono restare attive per settimane. Per questo le API retrocompatibili sono importanti anche se la migrazione del DB è “sicura”.

Tratta i nuovi dati come opzionali all'inizio. Aggiungi nuovi campi a richieste e risposte, ma non li richiedere dal primo giorno. Se un client vecchio non invia il campo nuovo, il server dovrebbe comunque accettare la richiesta e comportarsi come prima.

Evita di cambiare il significato dei campi esistenti. Rinominare un campo può andare bene se mantieni funzionante il nome vecchio. Riutilizzare un campo per un nuovo significato è dove avvengono rotture sottili.

I default server-side sono la tua rete di sicurezza. Quando introduci una colonna come preferred_language, imposta un default sul server quando manca. La risposta API può includere il campo nuovo e i client vecchi possono ignorarlo.

Regole di compatibilità che prevengono la maggior parte degli outage:

  • Aggiungi nuovi campi come opzionali prima, poi impone dopo l'adozione.
  • Mantieni il comportamento vecchio stabile, anche se aggiungi un comportamento migliore dietro un flag.
  • Applica default sul server così i client vecchi possono omettere i campi nuovi.
  • Assumi traffico misto e testa entrambi i percorsi: “il client nuovo lo invia” e “il client vecchio lo omette”.
  • Mantieni messaggi d'errore e codici d'errore stabili così il monitoring non diventa rumoroso improvvisamente.

Esempio: aggiungi company_size al flusso di signup. Il backend può impostare un default come “unknown” quando il campo manca. I client nuovi inviano il valore reale, quelli vecchi continuano a funzionare e i dashboard restano leggibili.

Quando la tua app si rigenera: mantenere schema e logica sincronizzati

Se la tua piattaforma rigenera l'applicazione, ottieni una ricostruzione pulita di codice e configurazione. Questo aiuta con le modifiche zero-downtime perché puoi fare piccoli passi additivi e ridistribuire spesso invece di tenere patch per mesi.

La chiave è una sola fonte di verità. Se lo schema DB cambia in un posto e la logica business in un altro, il drift avviene in fretta. Decidi dove definire i cambiamenti e tratta tutto il resto come output generato.

Naming chiaro riduce gli incidenti durante rollout a fasi. Se introduci un campo nuovo, rendi ovvio quale è sicuro per i client vecchi e quale è il percorso nuovo. Per esempio, nominare una colonna status_v2 è più sicuro di status_new perché ha senso anche fra sei mesi.

Cosa ritestare dopo ogni rigenerazione

Anche quando i cambiamenti sono additivi, una ricostruzione può far emergere accoppiamenti nascosti. Dopo ogni rigenerazione e deploy, ricontrolla un piccolo set di flussi critici:

  • Signup, login, reset password, refresh token.
  • Azioni core di create e update (quelle usate di più).
  • Controlli admin e permessi.
  • Pagamenti e webhook (per esempio, eventi Stripe).
  • Notifiche e messaggistica (email/SMS, Telegram).

Pianifica i passaggi di migrazione prima di aprire l'editor: aggiungi il campo nuovo, distribuisci con entrambi i campi supportati, esegui il backfill, passa alle letture, poi ritira il percorso vecchio più tardi. Questa sequenza mantiene schema, logica e codice generato allineati così i cambiamenti restano piccoli, revisionabili e reversibili.

Errori comuni che causano outage (e come evitarli)

Costruisci pensando ai client misti
Crea un backend pronto per la produzione con modellazione PostgreSQL e cambiamenti API compatibili con le versioni.
Inizia a creare

La maggior parte degli outage durante modifiche zero-downtime non sono causati dal “lavoro duro” sul database. Derivano dal cambiare il contratto tra database, API e client nell'ordine sbagliato.

Trappole comuni e mosse più sicure:

  • Rinominare una colonna mentre il codice vecchio la legge ancora. Mantieni la colonna vecchia, aggiungine una nuova e mappa entrambe per un po' (scrivi su entrambe, o usa una view). Rinomina solo quando puoi dimostrare che nessuno dipende più dal nome vecchio.
  • Rendere richiesto un campo nullable troppo presto. Aggiungi la colonna come nullable, distribuisci codice che la scrive ovunque, backfilla le righe vecchie e poi impone NOT NULL con una migrazione finale.
  • Eseguire il backfill in una singola transazione massiva che locka le tabelle. Backfilla a piccoli batch, con limiti e pause. Traccia il progresso così puoi riprendere in sicurezza.
  • Cambiare le letture prima che le scritture producano i nuovi dati. Cambia prima le scritture, poi backfilla, poi cambia le letture. Se le letture cambiano prima ottieni schermate vuote, totali sbagliati o errori di “campo mancante”.
  • Rimuovere campi vecchi senza prova che i client siano scomparsi. Mantieni i campi vecchi più a lungo di quanto pensi. Rimuovi solo quando le metriche mostrano che le versioni vecchie sono effettivamente inattive e hai comunicato una finestra di deprecazione.

Se rigeneri l'app, è tentante “pulire” nomi e vincoli in un colpo solo. Resisti. Il cleanup è l'ultimo passo, non il primo.

Una buona regola: se un cambiamento non può essere tranquillamente portato avanti e invertito, non è pronto per la produzione.

Monitoraggio e pianificazione del rollback per migrazioni a fasi

Trasforma il rollout in passi
Usa strumenti visuali per aggiungere campi, eseguire backfill in sicurezza e mantenere i client vecchi funzionanti.
Crea app

Le modifiche zero-downtime si vincono o si perdono su due cose: cosa osservi e quanto velocemente puoi fermare.

Monitora segnali che riflettono l'impatto reale sull'utente, non solo “il deploy è finito”:

  • Tasso di errori API (soprattutto picchi 4xx/5xx sugli endpoint aggiornati).
  • Query lente (p95 o p99 per le query sulle tabelle toccate).
  • Latenza delle scritture (quanto impiegano insert e update durante i picchi).
  • Profondità delle code (job che si accumulano per backfill o elaborazione eventi).
  • Pressione su CPU/IO del database (qualsiasi balzo dopo il cambiamento).

Se fai dual writes, aggiungi logging temporaneo che confronta i due. Mantienilo ristretto: logga solo quando valori divergono, includi l'ID del record e un breve codice motivo, e campiona se il volume è alto. Crea un promemoria per rimuovere questo logging dopo la migrazione così non diventi rumore permanente.

Il rollback deve essere realistico. Di solito non torni indietro con lo schema. Torni indietro con il codice e lasci lo schema additivo.

Un runbook pratico di rollback:

  • Reverti la logica applicativa all'ultima versione nota buona.
  • Disabilita prima le nuove letture, poi le nuove scritture.
  • Mantieni nuove tabelle o colonne ma smetti di usarle.
  • Metti in pausa i backfill finché le metriche non tornano stabili.

Per i backfill, costruisci un interruttore di stop che puoi premere in pochi secondi (feature flag, valore di config, pausa del job). Comunica anche le fasi in anticipo: quando iniziano le dual write, quando gira il backfill, quando cambiano le letture e cosa significa “stop” così nessuno improvvisa sotto pressione.

Lista di controllo rapida prima del deploy

Poco prima di spedire una modifica allo schema, fermati e fai questo controllo rapido. Cattura quelle piccole assunzioni che diventano outage quando ci sono versioni client miste.

  • Il cambiamento è additivo, non distruttivo. La migrazione aggiunge solo tabelle, colonne o indici. Nulla è rimosso, rinominato o reso più restrittivo in modo da rifiutare scritture vecchie.
  • Le letture funzionano con entrambe le forme. Il nuovo codice server gestisce sia “campo nuovo presente” sia “campo nuovo mancante” senza errori. I valori opzionali hanno default sicuri.
  • Le scritture restano compatibili. I client nuovi possono inviare dati nuovi, i client vecchi inviano payload vecchi e hanno successo. Se devono coesistere, il server accetta entrambi i formati e produce risposte parsabili dai client vecchi.
  • Il backfill si può fermare e riprendere. Il job gira a batch, si riavvia senza duplicare o corrompere dati e mostra un numero misurabile di “righe rimaste”.
  • Conosci la data di cancellazione. C'è una regola concreta su quando è sicuro rimuovere campi o logica legacy (per esempio, dopo X giorni più conferma che Y% delle richieste venga da client aggiornati).

Se usi una piattaforma che rigenera, aggiungi un ulteriore controllo: genera e distribuisci una build esattamente dal modello che stai migrando, poi conferma che l'API e la logica generate tollerino ancora i record vecchi. Un fallimento comune è presumere che il nuovo schema implichi logica richiesta nuova.

Scrivi anche due azioni rapide da eseguire se qualcosa va storto dopo il deploy: cosa monitorare (errori, timeout, progresso del backfill) e cosa revertare prima (spegnere il feature flag, mettere in pausa il backfill, revertare la release server). Questo trasforma “reagire velocemente” in un piano reale.

Esempio: aggiungere un campo nuovo mentre le app mobili vecchie sono ancora attive

Spedisci cambi di database più sicuri
Progetta modello dati e logica di business insieme così le release restano compatibili.
Costruisci backend

Gestisci un'app di ordini. Ti serve un campo nuovo, delivery_window, che sarà richiesto per nuove regole di business. Il problema è che build iOS e Android più vecchie sono ancora in uso e non invieranno quel campo per giorni o settimane. Se rendi il DB obbligatorio subito, quei client inizieranno a fallire.

Un percorso sicuro:

  • Fase 1: Aggiungi la colonna come nullable, senza vincoli. Mantieni letture e scritture esistenti.
  • Fase 2: Dual write. I client nuovi (o il backend) scrivono il campo nuovo. I client vecchi continuano a funzionare perché la colonna accetta null.
  • Fase 3: Backfill. Popola delivery_window per le righe vecchie usando una regola (inferisci dal metodo di spedizione, o usa “anytime” finché il cliente non lo modifica).
  • Fase 4: Cambia le letture. Aggiorna API e UI per leggere delivery_window prima, ma fai fallback al valore inferito quando manca.
  • Fase 5: Impone più tardi. Dopo adozione e backfill completi, aggiungi NOT NULL e rimuovi il fallback.

Cosa percepiscono gli utenti durante ogni fase resta noioso (questo è l'obiettivo):

  • Gli utenti mobili vecchi possono ancora effettuare ordini perché l'API non rifiuta i dati mancanti.
  • Gli utenti nuovi vedono il nuovo campo e le loro scelte vengono salvate coerentemente.
  • Support e ops vedono il campo riempirsi gradualmente, senza gap improvvisi.

Un gate di monitoraggio semplice per ogni passo: traccia la percentuale di nuovi ordini con delivery_window non null. Quando resta costantemente alta (e gli errori di validazione per “campo mancante” sono vicini allo zero), di solito è sicuro passare da backfill a imporre il vincolo.

Passi successivi: costruisci un playbook di migrazione ripetibile

Una rollout attento una-tantum non è una strategia. Tratta le modifiche allo schema come routine: stessi passi, stessa nomenclatura, stessi OK. Così la prossima modifica additiva resta noiosa, anche quando l'app è impegnata e i client sono su versioni diverse.

Mantieni il playbook breve. Deve rispondere: cosa aggiungiamo, come lo distribuiamo in sicurezza e quando rimuoviamo le parti vecchie.

Un template semplice:

  • Solo aggiunte (nuova colonna/tabella/indice, nuovo campo API opzionale).
  • Spedisci codice che può leggere entrambe le forme.
  • Backfill a piccoli batch, con un segnale chiaro di “fatto”.
  • Cambia il comportamento con un feature flag o config, non con un redeploy.
  • Rimuovi campi/endpoint vecchi solo dopo una data di cutoff e verifica.

Inizia con una tabella a basso rischio (uno status opzionale nuovo, un campo note) e percorri l'intero playbook: cambiamento additivo, backfill, client in versione mista e poi cleanup. Quella prova pratica mette in luce gap in monitoraggio, batching e comunicazione prima di tentare una grande riprogettazione.

Una buona abitudine che previene confusione a lungo termine: traccia le voci “rimuovere dopo” come lavoro reale. Quando aggiungi una colonna temporanea, codice di compatibilità o logica di dual-write, crea subito un ticket di cleanup con un owner e una data. Tieni un piccolo appunto di “debito di compatibilità” nei documenti di rilascio così resta visibile.

Se costruisci con AppMaster, puoi trattare la rigenerazione come parte del processo di sicurezza: modella lo schema additivo, aggiorna la logica di business per gestire sia i campi vecchi che quelli nuovi durante la transizione, e rigenera così il codice sorgente resta pulito man mano che i requisiti cambiano. Se vuoi vedere come questo workflow si adatta a un setup no-code che produce comunque codice reale, AppMaster (appmaster.io) è progettato attorno a questo stile di delivery iterativa e a fasi.

L'obiettivo non è la perfezione. È la ripetibilità: ogni migrazione ha un piano, una metrica e una rampa di uscita.

FAQ

Cosa significa davvero “zero-downtime” per una modifica allo schema?

Zero-downtime significa che gli utenti possono continuare a lavorare normalmente mentre cambi lo schema e distribuisci codice. Include evitare interruzioni evidenti, ma anche rotture silenziose come schermate vuote, valori errati, crash di job o scritture bloccate da lock lunghi.

Perché le modifiche allo schema rompono le cose anche quando la migrazione riesce?

Perché molte parti del sistema dipendono dalla forma del database, non solo dall'interfaccia principale. Job in background, report, script amministrativi, integrazioni e app mobili vecchie possono continuare a inviare o aspettarsi campi obsoleti molto dopo il deploy del nuovo codice.

Perché le app mobili più vecchie sono un rischio così grande durante le migrazioni?

Perché build mobili più vecchie possono restare in uso per settimane, e alcuni client ritentano richieste più vecchie in seguito. La tua API deve accettare sia payload vecchi che nuovi per un po’ in modo che le versioni miste possano coesistere senza errori.

Qual è il tipo di modifica dello schema più sicuro da fare senza downtime?

Le modifiche additive di solito non rompono il codice esistente perché lo schema vecchio resta presente. Rinominare o cancellare è rischioso perché rimuove qualcosa che i client vecchi ancora leggono o scrivono, causando crash o richieste fallite.

Come aggiungo un nuovo campo obbligatorio senza rompere i client vecchi?

Aggiungi prima la colonna come nullable così il codice vecchio può continuare a inserire righe. Esegui il backfill delle righe esistenti a batch, poi solo quando la copertura è alta e le scritture nuove sono coerenti imponi NOT NULL come passo finale.

Qual è una sequenza di rollout pratica per una migrazione zero-downtime?

Trattala come un rollout: aggiungi uno schema compatibile, deploya codice che supporta entrambe le versioni, esegui backfill a piccoli batch, cambia le letture con un fallback e rimuovi il campo vecchio solo quando puoi dimostrare che non viene più usato. Ogni step deve funzionare da solo.

Come posso eseguire un backfill senza causare lock o rallentamenti?

Esegui il backfill a piccoli batch con transazioni brevi per non bloccare le tabelle o sovraccaricare il DB. Rendi il job riavviabile e idempotente aggiornando solo le righe mancanti e tracciando il progresso in modo da poter mettere in pausa e riprendere in sicurezza.

Come mantengo la mia API compatibile mentre lo schema cambia?

Rendi i nuovi campi opzionali all'inizio e applica default sul server quando mancano. Mantieni stabile il comportamento vecchio, evita di cambiare il significato di campi esistenti e testa entrambe le strade: “il client nuovo lo invia” e “il client vecchio lo omette”.

Qual è il miglior piano di rollback durante una migrazione a fasi?

Nella maggior parte dei casi reverti il codice applicativo, non lo schema. Mantieni le colonne/tabelle additive, disabilita prima le nuove letture, poi le nuove scritture, e metti in pausa i backfill finché le metriche non tornano stabili per recuperare senza perdita di dati.

Cosa dovrei monitorare per sapere che è sicuro passare alla fase successiva?

Monitora segnali di impatto reale sull'utente come tassi di errore, query lente, latenza delle scritture, profondità delle code e CPU/IO del DB. Procedi solo quando i metriche sono stabili e la copertura del nuovo campo è alta, poi programma il cleanup come lavoro reale, non come “poi”.

Facile da avviare
Creare qualcosa di straordinario

Sperimenta con AppMaster con un piano gratuito.
Quando sarai pronto potrai scegliere l'abbonamento appropriato.

Iniziare