← Natrag
Tehnologija

Schema migracije bez downtime-a: DDL u produkciji bez gašenja sustava

RiNET Engineering· 19. lipnja 2026.· 5 min čitanja· 18 pregleda
Schema migracije bez downtime-a: DDL u produkciji bez gašenja sustava
🎧 Poslušaj članak

Kad god netko kaže da je dodavanje kolumne u PostgreSQL trivijalna operacija, vjerojatno nikad nije pokušao izvesti NOT NULL constraint na tablici od 50 milijuna redova dok aplikacija aktivno piše. U RiNET Engineeringu smo prošli kroz dovoljno tih noćnih mora da smo razvili sistem koji počiva na jednoj jednostavnoj premisi: DDL u produkciji nije jedan korak, nego sekvenca od barem pet koraka, od kojih svaki ima rollback proceduru. Ovdje nema mjesta herojstvu, samo proceduri. Počnimo od najčešćeg scenarija — trebate dodati kolumnu koja mora biti NOT NULL, ali podaci ne postoje.

Prva greška koju vidimo kod mlađih timova je pokušaj izvršavanja ALTER TABLE ADD COLUMN sa NOT NULL direktno na produkcijskoj tablici. PostgreSQL će to blokirati ako tablica nije prazna, jer ne zna koju vrijednost da stavi u postojeće redove. Ljudi onda panično brišu migraciju ili, još gore, dodaju DEFAULT vrijednost koja možda nije semantički ispravna. Rješenje je trivijalno, ali zahtijeva strpljenje: prvo dodajemo kolumnu kao nullable, zatim popunjavamo podatke u batchovima, i tek na kraju postavljamo NOT NULL. Svaki korak je reverzibilan, i svaki se može izvoditi dok aplikacija normalno radi.

Konkretno, kod izgleda ovako: ALTER TABLE orders ADD COLUMN processed_at timestamptz. Ovo je trenutna operacija, ne zaključava tablicu osim na razini metapodataka. Zatim slijedi backfill — UPDATE orders SET processed_at = NOW() WHERE processed_at IS NULL LIMIT 10000. Pokrećemo ovo u petlji, s provjerom broja pogođenih redova, dok ne obradimo sve. Bitno je koristiti LIMIT i raditi u manjim transakcijama, jer svaki UPDATE zaključava redove i može usporiti aplikacijske upite. Na instanci s 32 GB RAM-a i SSD diskovima, batch od 10 tisuća redova traje ispod 100 milisekundi.

Nakon što backfill završi, slijedi kritični trenutak: ALTER TABLE orders ALTER COLUMN processed_at SET NOT NULL. Ova operacija zahtijeva ACCESS EXCLUSIVE lock na tablici, što znači da će svi upiti biti blokirani dok se izvršava. Međutim, PostgreSQL je pametan — ako kolumna već ima vrijednosti u svim redovima, lock traje samo nekoliko milisekundi. Zato je backfill ključan. Ako biste preskočili taj korak i dodali DEFAULT uz NOT NULL, PostgreSQL bi morao fizički prepisati svaki red, što bi zaključalo tablicu na minute ili sate.

Ono što nas je naučilo najviše lekcija je backup strategija prije destruktivnih DDL operacija. Prije svakog ALTER TABLE koji briše kolumnu, mijenja tip podatka ili postavlja constraint, radimo CREATE TABLE orders_backup_20250115_1430 AS SELECT * FROM orders. Ovo nije samo paranoja — jednom smo imali slučaj gdje je DROP COLUMN slučajno uklonio pogrešnu kolumnu jer je netko kopirao SQL iz pogrešnog brancha. Backup nam je omogućio da u roku od pet minuta vratimo podatke, bez potrebe za restoreom iz WAL arhive. Naming pattern _backup_YYYYMMDD_HHMM je standard koji smo uveli prije tri godine i više nema rasprava o tome.

Backup tablice prije DDL-a ima dodatnu prednost — možete testirati migraciju na kopiji. Nakon što napravite backup, izvedite DDL na backup tablici i provjerite da li radi. Ovo je posebno korisno kod promjene tipova podataka, gdje PostgreSQL implicitne konverzije mogu raditi neočekivane stvari. Na primjer, ALTER TABLE orders ALTER COLUMN total TYPE numeric(10,2) USING total::numeric — ovo radi, ali ako total sadrži vrijednosti s više od dvije decimale, dobit ćete rounding error. Backup vam omogućuje da to otkrijete prije nego što zaključate produkcijsku tablicu.

Ovaj pristup razvijamo na rinet.one — za detalje slobodno se javi.

Pattern koji smo razvili za složene migracije uključuje privremene tablice. Recimo da trebate promijeniti strukturu tablice orders — dodati partitioning po datumu ili promijeniti primarni ključ. Umjesto da radite DROP i CREATE, napravite novu tablicu orders_new sa željenom shemom, zatim u batchovima kopirate podatke iz stare tablice, koristeći INSERT ... ON CONFLICT DO NOTHING za deduplikaciju. Nakon što su svi podaci preneseni, preimenujete tablice u transakciji: BEGIN; ALTER TABLE orders RENAME TO orders_old; ALTER TABLE orders_new RENAME TO orders; COMMIT. Ovo je zero-downtime tehnika koju koristimo za sve veće promjene.

Bitno je napomenuti da preimenovanje tablica nije potpuno transparentno za aplikaciju. Ako imate foreign key constraintove koji referenciraju staru tablicu, morate ih rekreirati. Također, pogledi, procedure ili trigeri koji koriste staro ime tablice moraju biti ažurirani. Zato prije ovakvih migracija radimo dependency check: SELECT * FROM information_schema.table_constraints WHERE table_name = 'orders' — i pregledamo sve objekte koji ovise o tablici. Ovo je dosadno, ali sprječava situacije gdje aplikacija dobiva grešku "relation orders does not exist" u sredini radnog dana.

Jedna od stvari koju smo naučili iz vlastitih grešaka je važnost monitoringa tijekom migracije. Prije izvođenja bilo kakvog DDL-a, postavljamo alerte na latency aplikacijskih upita koji čitaju iz te tablice. Ako vidimo da prosječno vrijeme odgovora skoči s 2 na 200 milisekundi, odmah prekidamo migraciju. PostgreSQL je dobar u balansiranju, ali DDL operacije poput VACUUM FULL ili REINDEX mogu ozbiljno poremetiti performanse. Zato sve agresivne operacije radimo u periodima niskog prometa, a za kritične sustave koristimo pg_repack umjesto VACUUM FULL jer ne zahtijeva ACCESS EXCLUSIVE lock.

Konkretni brojevi iz naše prakse: migracija koja je uključivala dodavanje tri kolumne i jednog indeksa na tablicu s 12 milijuna redova trajala je ukupno 47 minuta, od čega je lock na tablici trajao 3 sekunde. To je postignuto jer smo backfill radili u pozadini, a NOT NULL constraint dodali tek kad smo potvrdili da nema NULL vrijednosti. Bez ove strategije, lock bi trajao barem 10 minuta, što bi za aplikaciju s 5000 istovremenih korisnika bilo katastrofalno. Svaki put kad netko predloži "ajmo samo ALTER TABLE i gotovo", pokažem im te brojke.

Što se tiče naming konvencija, osim backup tablica, koristimo i privremene indekse s prefiksom _tmp. Na primjer, ako trebamo dodati UNIQUE constraint na kolumnu koja trenutno ima duplikate, prvo kreiramo indeks: CREATE UNIQUE INDEX CONCURRENTLY _tmp_orders_order_id_unique ON orders(order_id) WHERE order_id IS NOT NULL. Ovaj indeks kreiramo bez locka, a zatim ga koristimo da lociramo duplikate. Tek nakon što očistimo podatke, dodajemo pravi constraint: ALTER TABLE orders ADD CONSTRAINT orders_order_id_unique UNIQUE USING INDEX _tmp_orders_order_id_unique. Ovo je elegantan pattern koji izbjegava duge lockove.

Na kraju, najvažnija lekcija koju smo internalizirali je da schema migracije nisu tehnički problem, nego problem discipline. Tehnologija je tu — PostgreSQL nudi CONCURRENTLY opcije, partial indekse, i mogućnost rada u transakcijama. Ono što razlikuje dobar tim od lošeg je konzistentno poštivanje procedura: backup prije svakog destruktivnog DDL-a, backfill u batchovima, testiranje na kopiji, i monitoring tijekom izvođenja. Ovo su pravila koja smo platili krvlju, i ne namjeravamo ih mijenjati. Sljedeći put kad budete pisali ALTER TABLE, sjetite se da je baza podataka poput nuklearnog reaktora — moćna, ali nemilosrdna prema onima koji preskaču procedure.

Ovo gradimo svaki dan na RiNET-u.

Ako tvoja institucija, banka, osiguranje ili infrastrukturna firma rješava taj tip problema — zakaži razgovor. Pokazat ćemo ti arhitekturu, ne marketing.

database migrationszero downtimeDDLPostgreSQLschema evolutionRi.NETsovereign AIAI operativni sustavCroatian AItehnologija

Zanima vas ovakva AI tehnologija?

RiNET gradi sovereign AI rjesenja za javni sektor i poduzeca - od civic intelligence do automatizacije nabave.

Kontaktirajte nas →
0 komentara

Komentari