-- PAS 1: Adăugăm coloana fără a bloca tabelul
ALTER TABLE users ADD COLUMN phone_new VARCHAR(20) NULL;
-- PAS 2: Exemplu de backfill în batch-uri (pseudo-cod/SQL)
-- Se rulează repetat până când nu mai sunt rânduri de procesat
UPDATE users
SET phone_new = phone_old
WHERE id IN (
SELECT id FROM users
WHERE phone_new IS NULL
AND phone_old IS NOT NULL
LIMIT 5000
);
-- PAS 3: După flip și teste, facem curățenie
ALTER TABLE users DROP COLUMN phone_old;
ALTER TABLE users ALTER COLUMN phone_new SET NOT NULL;M-am ars de destule ori cu ALTER TABLE pe tabele mari ca să știu că "merge repede pe staging" e cea mai mare minciună pe care ne-o spunem ca developeri. La un proiect mai vechi, cu un tabel de vreo 25 de milioane de rânduri, un simplu add column cu o valoare default mi-a blocat tot site-ul timp de 12 minute. Clienții urlau, managementul era în spatele meu, iar eu așteptam transpirat să termine Postgres treaba. De atunci, am jurat că nu mai fac migrări distructive direct pe coloane vii.
Dacă lucrezi la o aplicație care trebuie să stea sus 24/7, nu poți să-ți permiți luxul unui lock de lungă durată pe un tabel central. Pattern-ul ăsta în 4 pași, numit uneori și Expand/Contract, e singura metodă prin care poți dormi liniștit când dai deploy vineri la prânz. Nu e neapărat cel mai rapid mod de a scrie cod, dar e cel mai sigur.
Pasul 1: Add (Coloana nouă e „invizibilă”)
Primul pas e să adaugi coloana nouă, dar cu o regulă de aur: trebuie să fie nullable. Dacă încerci să adaugi o coloană NOT NULL cu o valoare default pe un tabel mare, baza de date va trebui să scrie acea valoare în fiecare rând de pe disc. Asta înseamnă lock și mult I/O.
Am avut un caz unde am economisit cam 30% din timpul de deploy doar lăsând coloana nullable la început. Codul tău de producție încă nu știe de ea, deci nu se schimbă nimic pentru utilizatori. E doar o pregătire de teren.
Pasul 2: Dual Write și Backfill
Aici e unde majoritatea se grăbesc și o sfeclesc. Modifici aplicația să scrie în AMBELE coloane (cea veche și cea nouă). Tot ce intră nou de acum încolo va fi prezent în ambele locuri.
În paralel, rulezi un script de backfill pentru datele vechi. Atenție mare aici: nu rula un UPDATE uriaș pe tot tabelul. Am pățit la un proiect cu 8k useri activi să blocăm transaction log-ul pentru că am vrut să migrez totul dintr-o dată. Corect e să faci update-ul în batch-uri mici (de exemplu, 2000-5000 de rânduri) cu un mic „sleep” între ele. Durează mai mult? Da. Riscă să pună baza de date în genunchi? Nu.
Pasul 3: The Flip (Schimbăm citirea)
După ce scriptul de backfill s-a terminat și ești sigur că datele din coloana nouă sunt identice cu cele din coloana veche, schimbi logica de citire în aplicație. Acum, codul tău citește din coloana nouă, dar continuă să scrie în ambele.
De ce mai scriem în ambele? Pentru că dacă descoperi un bug după 10 minute, poți face rollback la codul anterior instant. Dacă te opreai din scris în coloana veche, rollback-ul ar fi însemnat pierdere de date pentru acele 10 minute.
Pasul 4: Cleanup (Ștergem trecutul)
Ultimul pas e cel mai satisfăcător. După ce aplicația a rulat câteva zile fără probleme pe noua structură, scoți scrierea duală și, în final, ștergi coloana veche.
Există un trade-off sincer aici: metoda asta e plictisitoare și consumă mult timp de developement. Trebuie să treci prin două sau trei runde de deploy pentru o singură schimbare de schema. Mai mult, consumi spațiu dublu pe disc pentru coloana respectivă pe durata migrării. Dar, comparativ cu stresul unui downtime neplanificat și cu explicațiile date la post-mortem, e un preț mic de plătit.
Folosiți vreun tool automat pentru asta (gen gh-ost sau pt-online-schema-change) sau preferați să controlați manual batch-urile de backfill din codul aplicației?