eduardweb.
Database & PrismaAvansat#migrations#database#postgres#system-design

Migrări de baze de date fără downtime: Pattern-ul expand-and-contract în 4 pași

De Cristian Barbu, 20 iun. 2026 · 16 vizualizări · 2 like-uri

Postat 20 iun. 2026
sql
DO $$
DECLARE
  rows_updated INT;
BEGIN
  LOOP
    -- Actualizăm în batch-uri de 1000 de rânduri
    UPDATE orders
    SET temp_phone = phone_number
    WHERE id IN (
      SELECT id FROM orders 
      WHERE temp_phone IS NULL AND phone_number IS NOT NULL
      LIMIT 1000
      FOR UPDATE SKIP LOCKED
    );
    
    GET DIAGNOSTICS rows_updated = ROW_COUNT;
    EXIT WHEN rows_updated = 0;
    
    COMMIT; -- Eliberăm lock-urile după fiecare batch
    PERFORM pg_sleep(0.05); -- Lăsăm DB-ul să respire
  END LOOP;
END $$;

Am pățit-o acum vreo 5 ani la un proiect cu o tabelă de comenzi de peste 12 milioane de rânduri. Am rulat un simplu ALTER TABLE cu o valoare default și am blocat complet scrierile timp de 15 minute groaznice. De atunci, m-am învățat minte: în producție, pe tabele mari, nu te joci cu migrări directe dacă vrei să ai 0 downtime.

Soluția pe care o folosesc acum se numește pattern-ul "Expand and Contract" (sau migrarea în 4 pași). Este plictisitoare, necesită disciplină și mai multe deployment-uri de cod, dar îți salvează jobul.

Ce este pattern-ul în 4 pași?

Ideea de bază e simplă: nu modifici niciodată o coloană direct. În schimb, adaugi o coloană nouă, copiezi datele în background, schimbi codul să o folosească și abia la final o elimini pe cea veche.

Pasul 1: Add (Expand)

Adaugi noua coloană în baza de date. Regula de aur: coloana trebuie să fie nullable sau să aibă un default care nu necesită rescrierea întregii tabele pe disc (în Postgres modern e mai simplu, dar pe MySQL mai vechi sau alte DB-uri poate fi critic). În acest moment, codul tău din producție nu știe de noua coloană și continuă să scrie și să citească din cea veche.

Pasul 2: Dual Write & Backfill

Modifici aplicația astfel încât să scrie în ambele coloane simultan (dual write), dar să citească în continuare din cea veche. Astfel, toate datele noi sunt salvate în ambele locuri.

Urmează partea grea: backfill-ul. Trebuie să copiezi datele vechi în noua coloană. Nu rula un UPDATE tabela SET coloana_noua = coloana_veche direct! Vei bloca tabela. Fă asta în batch-uri mici, în background, cu o mică pauză între ele.

Pasul 3: Flip

După ce backfill-ul s-a terminat și valorile sunt aliniate, schimbi codul aplicației. Acum, aplicația va citi exclusiv din coloana nouă și va scrie doar în ea. Recomand să lași scrierea duală activă încă 24-48 de ore, doar în caz că trebuie să faci rollback rapid fără pierdere de date. Dacă totul e ok, oprești dual write-ul definitiv.

Pasul 4: Remove (Contract)

După câteva zile de liniște în producție, poți șterge în siguranță coloana veche (ALTER TABLE DROP COLUMN). Tabela ta s-a contractat înapoi la dimensiunea normală, iar migrarea s-a făcut fără ca vreun utilizator să vadă vreo eroare de timeout.

Trade-off-uri sincere: Merită efortul?

Să fim sinceri: e un chin pe partea de proces. Pentru un tabel mic, de sub 100.000 de rânduri, e pierdere de vreme. Rulezi migrarea direct într-o seară și gata.

Dar la volume mari, trade-off-ul e clar:

  • Pro: 0 downtime real, risc de rollback aproape de zero, dormi liniștit noaptea.
  • Contra: Procesul durează zile în loc de secunde, ai nevoie de cel puțin 2-3 deployment-uri de cod separate și consumi temporar mai mult spațiu pe disc.

Voi cum gestionați migrările astea pe tabele mari? Mergeți pe varianta asta manuală sau folosiți unelte dedicate de tip gh-ost sau pt-online-schema-change?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

Doar membrii comunității pot lăsa comentarii.