-- Pasul 2: Exemplu de query pentru backfill în batch-uri (PostgreSQL)
-- Rulează într-o buclă din aplicație până când rândurile afectate devin 0
UPDATE payments
SET metadata_v2 = jsonb_build_object('source', 'legacy', 'data', metadata)
WHERE id IN (
SELECT id
FROM payments
WHERE metadata_v2 IS NULL
LIMIT 5000
);Am avut acum doi ani un proiect cu o tabelă de plăți care strânsese vreo 15 milioane de rânduri. Trebuia să migrăm o coloană de metadata dintr-un string JSON chior într-un format parțial structurat, plus redenumirea ei. Dacă dădeam un simplu ALTER TABLE cu RENAME și ceva procesare direct în migrare, blocam tot site-ul minute bune. În producție, asta înseamnă mii de euro pierduți și clienți furioși.
Să editezi scheme de baze de date live e ca și cum ai schimba roata la mașină în timp ce mergi pe autostradă. Soluția pe care o aplic de atunci se numește pattern-ul Expand and Contract (sau migrarea în 4 pași). Este sfântul graal al mentenanței de baze de date.
Pasul 1: Expand (Adăugarea noii coloane)
Primul pas este pur aditiv. Adaugi noua coloană în baza de date, dar o configurezi să fie nullable sau să aibă o valoare default stabilă.
În același timp, faci deploy la codul din aplicație care va face scriere dublă. Practic, de fiecare dată când aplicația creează sau actualizează o înregistrare, scrie datele atât în coloana veche, cât și în cea nouă. Totuși, aplicația va continua să citească exclusiv din coloana veche.
Prin pasul ăsta te asiguri că toate datele noi introduse de la acest moment încolo sunt deja în formatul cel nou.
Pasul 2: Backfill (Migrarea datelor istorice)
Aici e locul unde mulți își prind urechile și blochează baza de date. Ai milioane de rânduri vechi care au noua coloană goală. Nu rula niciodată un singur UPDATE tabela SET noua = veche pe o bază de date activă. O să declanșezi un lock masiv pe tabelă (lock escalation) și baza de date va refuza orice altă conexiune.
Soluția este să faci un script de backfill care rulează în background. Acesta selectează înregistrările în batch-uri mici (de exemplu, 5.000 de rânduri) bazate pe ID-uri și le actualizează treptat. Pui o mică pauză (un sleep de 50-100ms) între rulări ca să lași baza de date să respire și să deservească request-urile userilor.
Pasul 3: Flip (Trecerea pe noua coloană)
După ce scriptul de backfill s-a terminat și ești sigur că toate rândurile vechi au fost actualizate, faci următorul deploy la aplicație.
Acum schimbi logica de citire: aplicația va citi datele doar din noua coloană. Ca măsură de siguranță, eu prefer să las scrierea dublă activă încă 24-48 de ore. Dacă apare vreo problemă neprevăzută și trebuie să facem rollback la cod, datele salvate în acest interval vor fi disponibile și în coloana veche.
Pasul 4: Contract (Curățenia finală)
După ce sistemul a rulat stabil câteva zile pe noua coloană, e timpul să strângi jucăriile.
Faci deploy la un cod curat care nu mai știe nimic de coloana veche (elimini scrierea dublă). Ulterior, rulezi o migrare simplă de DROP COLUMN pentru a șterge coloana veche din baza de date și a elibera spațiul.
Trade-off-ul sincer
Sună ideal și extrem de sigur, dar vine cu un cost piperat. În loc de o migrare rapidă de 5 minute, procesul ăsta îți cere cel puțin 3 deployment-uri separate de cod, scrierea unui script de backfill pe care apoi îl arunci la gunoi și multă monitorizare.
Pentru un MVP sau un proiect cu 100 de useri, e un overkill masiv. Dar când ai mii de useri activi simultan și fiecare secundă de downtime costă bani grei, e singura cale corectă.
Voi cum gestionați migrările astea sensibile? Folosiți tool-uri dedicate sau tot pe scripturi custom de backfill mergeți?