2026-08-15veri4 dk

Veri göçü yazarken geri alınabilirlik

Bir migration'ın down() fonksiyonu olması onu geri alınabilir yapmıyor. Asıl soru şu: geri aldığınızda kaybolan bilgiyi nereden geri getireceksiniz?

Migration araçlarının çoğu size bir down() bloğu veriyor ve bu blok, geri alınabilirlik meselesini çözülmüş gibi gösteriyor. Çözmüyor. down() yalnızca şemayı eski haline döndürüyor; kaybolan veriyi geri getirmiyor. Bu iki şey aynı değil ve fark, üretimde gece yarısı fark ediliyor.

Sorulacak doğru soru şu: bu göç çalıştıktan sonra geri dönmek istersem, ihtiyacım olan bilgi hâlâ bir yerde duruyor mu?

İşlemler ikiye ayrılır

Bir göçteki her adım ya bilgi kaybettirir ya kaybettirmez. Ayrım bu kadar basit ve geri alınabilirlik tamamen bu ayrıma bağlı.

Bilgi kaybettirmeyenler: tablo eklemek, nullable kolon eklemek, index eklemek, kolonu genişletmek (varchar(50) → varchar(200)), yeni bir constraint'i NOT VALID olarak eklemek.

Bilgi kaybettirenler: kolon silmek, tablo silmek, tipi daraltmak, NOT NULL eklemek, veriyi yerinde dönüştürmek (UPDATE ... SET x = f(x)), iki kolonu birleştirmek.

Birinci gruptaki her şeyin down()'ı gerçekten geri alır. İkinci gruptakilerin down()'ı ise yalnızca şemayı kurar, kolonu geri getirir ama içi boştur. Şu ikisi arasındaki fark:

-- up
ALTER TABLE orders DROP COLUMN legacy_ref;

-- down
ALTER TABLE orders ADD COLUMN legacy_ref text;  -- kolon geldi, veri yok

Bu down() yeşil yanar, testten geçer, kimse bir şey demez. Geri dönüldüğünde legacy_ref her satırda NULL'dur.

Yıkıcı adımı ikiye bölün

Standart çözüm genişlet-daralt (expand/contract). Yıkıcı işlemi tek deploy'da yapmak yerine en az iki deploy'a yayarsınız ve arada bir geri dönüş penceresi bırakırsınız.

Kolon silmek için:

  1. Deploy A: kodu kolonu okumayacak ve yazmayacak hale getirin. Şemaya dokunmayın.
  2. Bekleyin. Bir deploy döngüsü, tercihen bir iş günü. Bu süre, geri dönme ihtiyacının ortaya çıkması için gereken süredir.
  3. Deploy B: kolonu düşürün.

A ile B arasındaki her an geri dönülebilir, çünkü veri hâlâ yerindedir. B'den sonra artık dönülemez, ama o noktada kolonun okunmadığından eminsinizdir.

Kolon yeniden adlandırmak, aynı şeyin en sık atlanan hali. RENAME COLUMN tek işlemde atomik görünür ama eski kodla yeni şema arasında uyumsuz bir an yaratır. Yerine: yeni kolonu ekleyin, çift yazın, geriye doldurun, okumayı çevirin, sonra eskisini düşürün.

-- 1
ALTER TABLE customers ADD COLUMN tax_number text;
-- 2  (uygulama iki kolona da yazar)
-- 3  (backfill, aşağıya bakın)
-- 4  (uygulama yeni kolondan okur)
-- 5
ALTER TABLE customers DROP COLUMN vergi_no;

Dönüştürmeden önce kopyalayın

Yerinde UPDATE en sinsi olanı, çünkü şema değişmez, dolayısıyla şema diff'i temiz görünür. Ama f(x) tersine çevrilebilir değilse bilgi gitmiştir.

Ucuz sigorta: dönüştürmeden önce eski değeri bir yere yazın.

CREATE TABLE _bk_20260815_prices AS
SELECT id, price FROM products;

UPDATE products SET price = round(price * 1.20, 2);

Yedek tabloyu tarihle adlandırın ve temizlemeyi ayrı bir işe bırakın. Bu tabloların birikmesi, geri dönemeyip veriyi kaybetmekten ucuzdur. Tek dikkat edilecek yer, kişisel veri içeren tabloların kopyasının saklama süresine tabi olması; onlarda kopyanın ömrünü baştan belirleyin.

Geriye doldurma göçün içinde olmasın

Milyonlarca satırlık bir UPDATE'i migration dosyasının içine koymak iki sorun üretir: deploy uzun sürer ve tablo kilitlenir. Ayrıca yarıda kesilirse nerede kaldığınızı bilemezsiniz.

Backfill'i ayrı çalıştırın ve iki özelliği olsun: parçalı ve yeniden çalıştırılabilir.

-- her turda tek parça; koşul, işlenmemiş satırı kendisi bulur
UPDATE customers
SET tax_number = vergi_no
WHERE tax_number IS NULL
  AND vergi_no IS NOT NULL
  AND id IN (
    SELECT id FROM customers
    WHERE tax_number IS NULL AND vergi_no IS NOT NULL
    ORDER BY id LIMIT 5000
  );

Bu sorgu kaç kez çalıştırılırsa çalıştırılsın aynı sonucu verir ve etkilenen satır sayısı sıfıra düştüğünde işi bitmiştir. Ayrı bir ilerleme tablosu tutmanız gerekmez, koşulun kendisi ilerlemedir.

Constraint'i iki adımda ekleyin

NOT NULL ya da yabancı anahtar eklemek, tabloyu baştan sona tarar ve bu tarama sırasında kilit tutar. Postgres tarafında iki adıma bölmek mümkün:

ALTER TABLE orders
  ADD CONSTRAINT orders_customer_fk
  FOREIGN KEY (customer_id) REFERENCES customers(id)
  NOT VALID;

-- ayrı bir adımda, daha hafif kilitle
ALTER TABLE orders VALIDATE CONSTRAINT orders_customer_fk;

Birinci adım yeni satırları kontrol etmeye başlar, ikinci adım eskileri doğrular. Aradaki bozuk satırları düzeltmek için zamanınız olur.

Asıl geri dönüş ileri doğrudur

Pratikte üretimde down() çalıştırıldığını görmek nadirdir. Bir şey bozulduğunda ekipler genelde şemayı geri sarmaz, düzeltici bir göç yazıp ileri gider. Bunu kabul etmek, göçleri yazma biçiminizi değiştirir: hedef down()'ın kusursuz olması değil, up()'ın düzeltilebilir bir durum bırakmasıdır.

Bir göçü yazdıktan sonra sorulacak üç soru:

  • Bu adım bilgi siliyor mu? Siliyorsa kopyası nerede?
  • Eski kod bu şemayla çalışır mı? Çalışmıyorsa deploy sırası nedir?
  • Yarıda kesilirse tekrar çalıştırılabilir mi?

Üçüne de cevabınız varsa göç geri alınabilir sayılır. down() bloğunun varlığı bu üç sorunun hiçbirine cevap değildir. Bir sonraki migration'ınızı açtığınızda bu üç satırı dosyanın başına yorum olarak yazın, cevaplayamadığınız satır o göçün riskidir.