-- Exemplu de backfill în batch-uri mici (PostgreSQL)
-- Evită blocarea tabelei prin limitarea numărului de rânduri modificate într-o tranzacție
CREATE OR REPLACE FUNCTION backfill_user_status()
RETURNS void AS $$
DECLARE
rows_updated int;
BEGIN
LOOP
UPDATE users
SET status_new = status_old
WHERE id IN (
SELECT id FROM users
WHERE status_new IS NULL
LIMIT 1000
);
GET DIAGNOSTICS rows_updated = ROW_COUNT;
COMMIT; -- Eliberăm lock-urile după fiecare batch
EXIT WHEN rows_updated = 0;
PERFORM pg_sleep(0.1); -- O mică pauză pentru a lăsa alte query-uri să ruleze
END LOOP;
END;
$$ LANGUAGE plpgsql;Am trecut acum ceva timp prin coșmarul numit „hai să facem deploy duminică noaptea la 3 ca să nu prindem trafic”. La un proiect cu peste 15 milioane de rânduri în tabela de useri, un simplu ALTER TABLE prost gândit ne-a blocat baza de date și ne-a ținut serviciul jos aproape 40 de minute. Atunci am zis „gata” și am trecut exclusiv la pattern-ul de migrare expand/contract în patru pași.
Dacă ai o bază de date care rulează în producție și nu-ți permiți să pierzi query-uri în timp ce modifici structura tabelelor, abordarea clasică cu „oprim aplicația, rulăm migrarea, pornim aplicația” este complet exclusă. Iată cum facem noi lucrurile acum, pas cu pas, ca să dormim liniștiți noaptea.
Ce înseamnă, de fapt, migrarea în 4 pași?
Ideea de bază e simplă: nu faci niciodată o modificare distructivă direct pe schema activă. În schimb, împarți procesul în patru etape distincte, rulate la distanță de câteva ore sau chiar zile între ele.
1. Add (Adăugarea noii structuri)
În prima fază, adaugi noua coloană sau noul tabel în baza de date. Regula de aur aici: coloana nouă trebuie să fie neapărat nullable sau să aibă o valoare implicită. Nu pui constrângeri de tip NOT NULL încă.
În acest stadiu, codul tău din producție habar nu are de noua coloană și continuă să scrie și să citească din cea veche. Riscul este zero.
2. Backfill (Scriere dublă și populare istoric)
Aici începe distracția. Faci deploy la o versiune de cod care scrie în ambele coloane (cea veche și cea nouă), dar citește în continuare doar din cea veche.
După ce ai pornit scrierea dublă, rulezi un script de migrare de date (backfill) pentru rândurile existente. Aici m-am ars de multe ori: dacă încerci să faci un UPDATE masiv pe milioane de rânduri dintr-o singură tranzacție, blochezi tabela și moare producția. Soluția? Faci update în batch-uri mici (de exemplu, câte 500 sau 1000 de rânduri o dată), cu o mică pauză între ele.
3. Flip (Mutarea citirii)
După ce ai verificat că toate datele vechi au fost copiate în noua coloană și că scrierea dublă funcționează corect, schimbi codul din aplicație să citească exclusiv din coloana nouă.
De ce e mișto pasul ăsta? Pentru că dacă descoperi un bug neprevăzut, poți face rollback instant la versiunea anterioară de cod. Datele din coloana veche sunt încă la zi, pentru că scrierea dublă a rămas activă.
4. Remove (Curățenia)
După ce ai lăsat aplicația să ruleze câteva zile în faza de „Flip” și ești 100% sigur că totul e stabil, poți scoate codul de scriere dublă. Aplicația acum folosește doar noua structură.
Ultimul pas e să ștergi coloana veche din baza de date printr-o migrare simplă. Felicitări, ai făcut o migrare majoră fără nicio secundă de downtime.
Compromisul sincer: merită efortul?
Mai sus sună totul frumos, dar hai să fim realiști. Procesul ăsta e incredibil de plictisitor și consumă mult timp. Ceea ce înainte rezolvai într-un singur PR de 10 linii acum necesită cel puțin 3 PR-uri separate, deploy-uri coordonate și scripturi de backfill monitorizate atent.
La un proiect mic, cu 10k useri și fără trafic concurent masiv, e o pierdere totală de timp. Acolo dai un ALTER TABLE direct și ai terminat în 2 secunde. Dar când ai clienți enterprise și fiecare minut de downtime înseamnă mii de euro pierduți, pattern-ul ăsta devine sfânt.
Voi cum gestionați migrările astea dureroase? Ați automatizat procesul de backfill sau încă rulați scripturi manuale din consolă?