Ana sayfa → Teknik
Şema Değişikliği: Cuma Gecesi Değil, İki Aşamada
Cuma 23:00. Migration başladı, “beş dakika sürer” demiştik. 23:40’ta hâlâ devam ediyordu ve o 40 dakika boyunca sipariş tablosuna kimse yazamadı. Sebebi basit: test veritabanında 80 bin satır vardı, canlıda 47 milyon.
- Şema değişikliği bir an değil, bir penceredir. Deploy süresince kodun iki sürümü aynı anda canlıdır; şema ikisini de aynı anda mutlu etmeli.
- Beş adım, beş ayrı sürüm. Ekle → çift yaz → doldur → okumayı çevir → sil. Her adım tek başına geri alınabilir.
- RENAME diye bir işlem yok. İki değişikliğin kılık değiştirmiş hâli: ekle ve sil. Aradaki her şey senin sorunun.
- Backfill tek UPDATE değildir. Parti parti, kesilebilir, replica gecikmesi izlenerek.
- Asıl soru “eski kolonu ne zaman sileceğiz”. Çoğu ekipte cevap “hiç”; bunu da tarih yazarak çözdük.
- Gece yapılan migration, gözden geçirilmemiş migration demektir. Gündüz yapılamayan değişiklik, henüz güvenli değil demektir.
Sahadan: 80 bin satır ile 47 milyon satır
Migration’ı yazan arkadaş her şeyi doğru yapmıştı. Değişiklik gözden geçirildi, test ortamında çalıştı, süre ölçüldü: 4,2 saniye. Cuma gecesi seçildi ki trafik düşük olsun. Kitapta yazan her şey yapılmıştı.
Kaçırdığımız tek şey şuydu: test veritabanı canlının küçültülmüş bir kopyası değildi, başka bir veritabanıydı. 80 bin satırda tablo yeniden yazmak saniyeler sürer, 47 milyon satırda dakikalar. Ölçtüğümüz sayı doğruydu ama ölçtüğümüz şey yanlıştı.
İkinci hata benim. “Cuma gecesi yapalım” dediğimde kendimi güvende hissettim; oysa o cümle riski azaltmıyor, sadece tanığı azaltıyor. Gece yapılan bir değişiklik daha az kişinin izlediği, daha yorgun kafayla düzeltilen bir değişikliktir. Riski düşürmek istiyorsan işlemi küçültmen gerekir, saati değil.
Neden tek hamlede olmuyor
Sebep veritabanında değil, deploy’da. Sürümü canlıya çıkarırken eski pod’lar bir anda kaybolmuyor; yeni sürüm ayağa kalkarken eski sürüm hâlâ istek karşılıyor. O pencere birkaç saniye de olabilir, kademeli çıkışta saatler de (sürüm yönetimi yazısında anlattığım düzende günler).
Yani şu an canlıda iki farklı kod sürümü aynı tabloya yazıyor. Şemayı tek hamlede yeni sürüme göre değiştirirsen, eski sürüm o an hata almaya başlıyor. Kural buradan çıkıyor: her şema değişikliği, hem eski hem yeni kodla çalışmak zorunda.
Expand / contract: beş adım
“Telefon numarasını tek alandan ülke kodu + numara olarak ayıralım” gibi basit bir istek, sahada şöyle görünüyor:
1) EXPAND : yeni kolonu ekle, NULL kabul etsin
ALTER TABLE musteri ADD COLUMN ulke_kodu varchar(5) NULL;
kod degismedi. geri alma: kolonu birak, zararsiz.
2) CIFT YAZ : kod yeni kayitta HER IKI kolona da yazar
okuma hala eski kolondan. bu surum tek basina cikar.
geri alma: onceki surume don, yeni kolon bos kalir.
3) BACKFILL : eski satirlari parti parti doldur (asagida)
kod degismedi. istedigin zaman durdur, devam et.
4) OKUMAYI CEVIR : kod yeni kolondan okumaya baslar
hala her ikisine yaziyor. bu en kritik surum:
geri alma tek adimda mumkun cunku eski kolon guncel.
5) CONTRACT : once cift yazmayi kaldir (bir surum),
sonra eski kolonu sil (ayri bir surum).
ALTER TABLE musteri DROP COLUMN telefon_eski;
bu adim GERI ALINAMAZ. en az bir hafta beklenir.
Beş adım fazla gibi görünüyor ama her biri sıradan bir sürüm; toplam iş, tek hamlelik migration’dan uzun sürmüyor. Fark şurada: hiçbir adımda kilit yok, hiçbir adımda “geri dönüş yok” anı yok. 4. adıma kadar her şey tek komutla geri alınıyor.
Dördüncü adımın neden kritik olduğunu kaçırmayalım: okumayı çevirdiğinde hâlâ çift yazıyorsun. Bu yüzden bir sorun çıkarsa okumayı eskiye geri almak yeterli — eski kolon güncel kalmaya devam ettiği için veri kaybı yok. Çift yazmayı okumayla aynı sürümde kaldıran ekipler, geri dönüşü olmayan bir kapıdan geçiyor.
Backfill: tek UPDATE değil
Üçüncü adımın klasik hatası şu tek satır:
# BOYLE YAPMA - 47 milyon satiri tek islemde kilitler
UPDATE musteri SET ulke_kodu = substring(telefon_eski, 1, 3);
# BOYLE YAP - parti parti, kesilebilir, izlenebilir
son_id = 0
while True:
parti = "UPDATE musteri SET ulke_kodu = substring(telefon_eski,1,3)
WHERE id > {son_id} AND ulke_kodu IS NULL
ORDER BY id LIMIT 5000
RETURNING id"
satirlar = calistir(parti)
if not satirlar: break
son_id = max(satirlar) # nerede kaldigini KAYDET
bekle(200) # replica'ya nefes aldir
if replica_gecikmesi() > 5: # saniye
bekle(5000) # yavasla, durma
Üç ayrıntı önemli: nerede kaldığını kaydet (backfill yarıda kesilir, devam edebilmeli), replica gecikmesini izle (backfill’in en sık yan etkisi okuma replica’larının geride kalması ve bunun kullanıcıya “kaydım görünmüyor” diye yansıması), partiler arasında bekle (kesintisiz yazmak diğer sorguları aç bırakır).
Hangi işlem güvenli, hangisi değil
| İşlem | Durum | Ne yapmalı |
|---|---|---|
| Kolon ekle (NULL kabul eden) | Güvenli | Doğrudan yapılabilir |
| Kolon ekle (NOT NULL + varsayılan) | Motora bağlı | Eski sürümlerde tablo yeniden yazılır; önce NULL ekle, doldur, sonra kısıtı koy |
| Kolon adını değiştir | Tehlikeli | Yapma. Ekle + sil olarak beş adıma böl |
| Kolon tipini değiştir | Tehlikeli | Yeni kolon aç, çift yaz, doldur, çevir, sil |
| Kolon sil | Geri alınamaz | Sadece contract adımında, en az bir hafta sonra |
| Index ekle | Kilitleyebilir | Eşzamanlı (concurrently) seçeneğiyle, işlem dışında |
| NOT NULL kısıtı ekle | Tabloyu tarar | Önce doğrulanabilir kısıt, sonra doğrula, sonra sıkılaştır |
Tablonun asıl mesajı şu: tek satırlık komutların çoğu tek adım değil.
RENAME bir kelime ama iki değişiklik. NOT NULL iki kelime ama
47 milyon satırlık bir tarama.
Eski kolonu ne zaman sileceğiz?
Bu sorunun ekiplerdeki gerçek cevabı “hiç”. Beşinci adım hiç gelmiyor, çünkü kimse acil değil. Bizde aynısı oldu: bir yıl sonra bakıldığında üç tabloda “eski” ekli kolonlar duruyordu; ikisine hâlâ çift yazılıyordu ve kimse neden olduğunu bilmiyordu.
Çözüm, feature flag’lerde işe yarayan kuralın aynısı oldu: karar, işi başlatırken verilir. Migration dosyasının başına bir satır koyduk ve CI bu tarihi izliyor:
# migration: 2026_04_12_ulke_kodu_ekle
# expand-contract: EVET
# eski kolon : musteri.telefon_eski
# silme tarihi : 2026-05-20 <-- CI bu tarihten sonra uyarir
# sahip : tek isim
Tarih geçince CI uyarı üretiyor, iki hafta sonra derlemeyi kırıyor. Sert ama tek işleyen yöntem bu; uyarıyla yetindiğimiz dönemde hiçbir kolon silinmedi.
Ne izlemeli?
| Ne | Neden |
|---|---|
| En uzun kilit süresi (migration başına) | Ortalama yalan söyler; tek bir 40 dakika bütün ayı bozar |
| Backfill sırasında replica gecikmesi | Kullanıcıya “kaydım yok” diye yansıyan tek şey |
| Tamamlanmamış expand/contract sayısı | Yarım kalmış her geçiş, çift yazma borcudur |
| Gece yapılan migration oranı | Yüksekse süreç değil, cesaret kullanıyorsun |
Bende işe yaramayanlar
- Test veritabanını büyütmek. “Canlı kadar veri koyalım” dedik; üç hafta sonra kimse o ortamı güncel tutmuyordu. İşe yarayan şey, migration’ı canlının bir kopyasında (restore edilmiş yedek) çalıştırmak ve süreyi oradan ölçmek oldu.
- Migration’ı uygulama deploy’undan ayırmamak. Aynı pipeline’da çalıştırdığımız sürece, migration yavaşlayınca deploy da takılıyordu ve geri alma karmaşıklaşıyordu. Ayırdık: şema değişikliği kendi başına çıkar, uygulama ayrı.
- “Bakım penceresi” ilan etmek. İlk refleks buydu. Ayda bir yarım saat kesinti planlamak, expand/contract öğrenmemek için ödediğimiz bedeldi — ve o pencere hep dolu geçti, hiçbir zaman yetmedi.
Kontrol listesi
- Bu değişiklik kodun eski sürümüyle de çalışıyor mu?
- Süreyi canlı boyutundaki bir kopyada mı ölçtüm, test ortamında mı?
- Kilit alıyor mu? Alıyorsa kaç saniye, hangi tablo?
- Adımlardan hangisi geri alınamaz — ve o adım ayrı bir sürümde mi?
- Backfill parti parti mi, kesilirse kaldığı yerden devam eder mi?
- Replica gecikmesini kim izliyor, eşiği ne?
- Eski kolonun silme tarihi yazılı mı?
- Bunu gündüz yapabiliyor muyum? Yapamıyorsam neden?
Sonuç
O cuma gecesi 40 dakika kaybettik ve ertesi hafta aynı değişikliği yeniden yaptık — bu sefer beş adımda, gündüz, kimse uyanık beklemeden. Toplam süre daha uzundu: dört gün. Kesinti süresi ise sıfırdı.
Şema değişikliğini zorlaştıran şey veritabanı değil, eski ve yeni kodun bir süre birlikte yaşamak zorunda olması. Bunu kabul ettiğin anda çözüm kendiliğinden geliyor: değişikliği, her adımı tek başına doğru olan küçük parçalara böl.
Test şu: bu migration’ı Salı öğlen, trafik en yüksekken çalıştırabilir miyim? Cevap hayırsa gece yapmak onu güvenli hâle getirmiyor — sadece kimsenin izlemediği bir saate taşıyor.