-- Pasul 1: Adăugăm noua coloană ca nullable
ALTER TABLE users ADD COLUMN phone_normalized VARCHAR(20) DEFAULT NULL;
-- Pasul 2: Script-ul de backfill rulat în batch-uri mici (pseudo-cod SQL)
UPDATE users
SET phone_normalized = REGEXP_REPLACE(phone_raw, '[^0-9+]', '', 'g')
WHERE id BETWEEN 1 AND 5000
AND phone_normalized IS NULL;
-- Pasul 4: După ce aplicația folosește doar noua coloană, o ștergem pe cea veche
ALTER TABLE users DROP COLUMN phone_raw;Am lucrat acum doi ani la un proiect cu peste 12 milioane de tranzacții în baza de date, unde un tabel blocat mai mult de 3 secunde genera timeout-uri în cascadă în tot sistemul. Atunci am învățat pe pielea mea că un simplu ALTER TABLE aruncat direct în producție e cel mai rapid mod de a-ți strica weekendul.
Dacă lucrezi cu baze de date PostgreSQL sau MySQL care au trecut de câteva sute de gigabytes, modificările de schemă devin periculoase. Soluția nu e să programezi mentenanțe noaptea, ci să folosești un pattern clar în patru pași: Add, Backfill, Flip, Remove.
De ce crapă migrarea clasică?
Când vrei să schimbi tipul unei coloane sau să adaugi una nouă cu o valoare default complexă, baza de date pune un lacăt (Access Exclusive Lock). În acel moment, toate scrierile și citirile pe tabelul respectiv stau la coadă. Dacă ai trafic mare, conexiunile din pool se epuizează rapid și aplicația crapă.
Trade-off-ul este simplu: în loc de o singură migrare rapidă dar riscantă, facem patru pași lenți, dar complet siguri pentru utilizatori.
Pasul 1: Adăugarea (Add)
Adăugăm noua coloană în baza de date ca fiind nullable (sau fără constrângeri stricte la început). În același timp, facem deploy la codul din aplicație (Deploy-ul 1) care începe să scrie în ambele coloane — cea veche și cea nouă.
Atenție: aplicația citește în continuare doar din coloana veche. Coloana nouă doar acumulează date proaspete.
Pasul 2: Popularea din spate (Backfill)
Avem coloana nouă, dar toate înregistrările istorice au NULL în dreptul ei. Acum rulăm un script în background care ia datele vechi, le procesează dacă e nevoie, și le scrie în coloana nouă.
La proiectul de care ziceam, am făcut backfill pe 8 milioane de rânduri folosind batch-uri mici, de câte 5000 de rânduri, cu o pauză de 100ms între ele. De ce? Ca să nu blocăm replicarea bazei de date și să nu încărcăm CPU-ul inutil.
Pasul 3: Comutarea (Flip)
După ce ne-am asigurat că toate rândurile vechi au datele migrate, facem Deploy-ul 2. Modificăm aplicația să citească și să scrie doar din/în coloana nouă.
Coloana veche rămâne neatinsă în baza de date. Dacă ceva nu merge bine sau observăm un bug de mapare în producție, putem face rollback instant la codul anterior, fără să pierdem date.
Pasul 4: Ștergerea (Remove)
După câteva zile în care totul a rulat impecabil în producție, facem curățenie. Facem Deploy-ul 3 care elimină complet orice referință din cod către vechea coloană. Abia după acest deploy, rulăm comanda SQL de ștergere a coloanei vechi din baza de date.
Costul real al acestei metode
Este evident că acest workflow cere disciplină și timp. Ai nevoie de cel puțin trei deploy-uri de cod și de scripturi de migrare scrise cu atenție. Pentru un MVP cu 100 de utilizatori, e un overkill masiv. Dar când ai clienți activi la orice oră și fiecare minut de downtime te costă bani grei, e singura cale rezonabilă.
Voi cum gestionați migrările mari? Mergeți pe varianta asta safe sau preferați să riscați cu un lock_timeout setat agresiv în Postgres?