-- Exemplu de backfill în batch-uri ca să eviți table lock-ul
-- Rulăm asta recursiv dintr-un script extern până când rândurile afectate devin 0
UPDATE users
SET phone_e164 = '+' || ltrim(phone, '0')
WHERE id IN (
SELECT id FROM users
WHERE phone_e164 IS NULL
AND phone IS NOT NULL
LIMIT 5000
);Ne-am lovit cu toții de faza asta: trebuie să schimbi structura unei tabele mari în producție, dar nu-ți permiți niciun minut de mentenanță. Dacă dai un simplu ALTER TABLE pe o tabelă cu milioane de rânduri, blochezi scrierile și ai pus aplicația în cap. Am pățit asta acum doi ani la un proiect cu peste 6 milioane de useri activi, unde fiecare secundă de downtime însemna pierderi financiare și alerte de PagerDuty în Slack.
Soluția nu e să speri că "merge repede", ci să folosești pattern-ul Expand and Contract. Practic, spargi o singură migrare distructivă în patru pași lenți, dar complet siguri.
Pasul 1: Expand (Adăugarea noii coloane și scrierea dublă)
Primul pas e complet inofensiv. Adaugi noua coloană (să zicem că trecem de la phone la phone_e164 pentru validare mai bună) ca fiind nullable. În același timp, modifici codul aplicației să scrie în ambele coloane la fiecare INSERT sau UPDATE.
Aplicația citește în continuare din coloana veche (phone), dar orice user nou creat sau modificat va avea datele salvate în ambele locuri. Baza de date nu simte niciun efort suplimentar semnificativ, iar riscul de lock-uri lungi e zero pentru că adăugarea unei coloane nullable în Postgres sau MySQL modern e o operațiune instantanee.
Pasul 2: Backfill (Umplem trecutul)
Acum ai date noi scrise în ambele coloane, dar ce facem cu cele 6 milioane de rânduri vechi? Aici intervine scriptul de backfill. Îl rulezi în background, în batch-uri mici (de exemplu, câte 5000 de rânduri o dată), cu o mică pauză între ele ca să nu sufoci CPU-ul bazei de date.
Am făcut greșeala în trecut să rulez un UPDATE uriaș pe toată tabela direct. Nu face asta. Îți va bloca tabela și vei avea downtime garantat. Mergi în tranșe mici, ideal noaptea sau în orele cu trafic redus.
Pasul 3: Flip (Mutarea citirii)
După ce scriptul de backfill s-a terminat și ai verificat că toate înregistrările vechi au acum valoarea copiată în noua coloană, e timpul pentru flip. Modifici codul aplicației să citească exclusiv din noua coloană (phone_e164).
În faza asta, aplicația încă scrie în ambele coloane (just in case). Dacă ceva crapă sau observi că noul format are bug-uri, poți face rollback instant la codul vechi fără să pierzi date, pentru că coloana veche e încă la zi. E plasa ta de siguranță.
Pasul 4: Contract (Ștergerea vechii coloane)
După câteva zile în care totul e stabil în producție și ești sigur că nu mai ai nevoie de rollback, oprești scrierea dublă din cod. Ultimul pas e să ștergi coloana veche prin DROP COLUMN.
Trade-off-ul sincer
Sună frumos și curat, dar vine cu un cost destul de mare de overhead mental și de proces. În loc de o singură comandă SQL și un singur deploy, acum ai nevoie de cel puțin 3 deploy-uri de cod separate și monitorizare atentă la fiecare pas. De asemenea, codul tău va fi temporar mai murdar din cauza logicii de dual-write.
Dacă ai un proiect mic, cu sub 50.000 de utilizatori, schema asta e overkill curat. Mai bine pui o pagină de mentenanță 30 de secunde duminică dimineața la ora 4 și ai rezolvat problema cu mult mai puțin stres. Dar când scara proiectului crește, abordarea asta devine singura cale prin care îți păstrezi jobul și liniștea în weekend.
Voi cum gestionați migrările astea mai delicate? Folosiți tool-uri dedicate de tip gh-ost sau preferați să scrieți logica direct în codul aplicației?