Güvenli Migration Yazma
Production'ı düşürmeyen, zero-downtime, reversible veritabanı migration'ları yazmayı öğreten skill — expand/contract pattern, büyük tablo, online DDL, backfill, rollback planı.
İçerik
Güvenli Migration Yazma
Veritabanı migration'ları production'ı düşürme potansiyeli taşır. Bu skill, zero-downtime, reversible ve büyük tabloya uygun migration yazmayı öğretir.
Ne zaman aktif olur
- Yeni tablo / kolon / index eklenirken
- Mevcut schema değiştirilirken (rename, type, NOT NULL)
- Büyük veri backfill gerektiğinde
- Eski kolonu / tabloyu kaldırırken
Çekirdek prensipler
1. Expand - Migrate - Contract (iki fazlı deploy)
Schema ile kod aynı anda değişmez. İki deploy:
Phase 1 (Expand): Schema yeni kolonu/tabloyu ekler. Kod hem eskiyi hem yeniyi yazar (backward compatible).
Phase 2 (Contract): Kod sadece yeni kolonu okur/yazar. Eski kolon drop edilir (sonraki deploy).
Arada backfill + monitor.
2. Migration'lar reversible olur
Her "up" için "down" yazılır. Sadece 10 saniye uğraşılır ama prod'da rollback gerektiğinde hayat kurtarır.
-- up
ALTER TABLE users ADD COLUMN email_verified_at TIMESTAMPTZ;
-- down
ALTER TABLE users DROP COLUMN email_verified_at;
Destructive bir şey ise down yazamasa bile down'da comment ile belirt:
-- down
-- NOT REVERSIBLE: drops column with data. Restore from backup if needed.
3. Büyük tabloda LOCK alma
Naif ALTER TABLE add_column NOT NULL DEFAULT ... prod'da 10M row'luk tabloya 10 dakika LOCK koyar — tüm istekler pending. Bunun yerine:
- Kolonu nullable + default yok ekle (anlık)
- Default value yazan trigger / uygulama katmanı geçici çözüm
- Backfill (chunk'lı)
- NOT NULL kısıtını sonra ekle
4. Tek migration tek iş
Bir migration bir şemasal değişiklik içerir. "10 ALTER + 3 UPDATE + 2 INSERT" tek dosyada olmaz — biri fail ederse hepsini rollback etmek zor.
Workflow — yeni migration yazarken
Adım 1: Sınıflandır
Migration tipi nedir?
→ Add column / table : DDL, genelde hızlı ama büyük tabloda dikkat
→ Drop column / table : Contract phase, kod önceden güncellenmiş olmalı
→ Add index : CONCURRENTLY (Postgres), online (MySQL)
→ Rename column : Expand/contract (hiçbir zaman direct rename)
→ Change type : Expand (yeni kolon) + backfill + contract
→ Add NOT NULL constraint : Önce default, sonra NOT NULL
→ Data-only (UPDATE/INSERT) : Batch, transaction outside DDL
Adım 2: Etki değerlendirmesi
- Tablo satır sayısı? (> 1M ise dikkat)
- Concurrent yazma var mı?
- LOCK süresi tahmini?
- Downtime kabul edilebilir mi?
- Rollback planı?
Adım 3: Yazma
Phase'leri ayır. Her phase ayrı migration dosyası.
Adım 4: Test
- Dev DB'de up + down test et
- Staging'de prod-size verisiyle test (canlı trafik altında idealde)
- Timing ölç, LOCK bekleme süresi çıkart
Adım 5: Deploy
- Off-peak saatte (trafik düşük)
- Monitor açık: replication lag, DB latency, error rate
- Rollback tetiği hazır
Pattern'ler
Ekleme: yeni kolon
-- ✅ İyi: anında tamamlanır
ALTER TABLE users ADD COLUMN phone TEXT;
-- ❌ Kötü: büyük tabloda LOCK uzun
ALTER TABLE users ADD COLUMN phone TEXT NOT NULL DEFAULT '';
Postgres 11+ ve MySQL 8+'da default değerli kolon ekleme metadata-only (hızlı), ama NOT NULL başka konu.
Ekleme: NOT NULL kısıtı
3 fazlı:
-- Phase 1: kolonu nullable ekle
ALTER TABLE users ADD COLUMN phone TEXT;
-- Phase 2: backfill (chunk'lı, kodda veya batch job)
UPDATE users SET phone = '' WHERE phone IS NULL LIMIT 10000;
-- tekrarla
-- Phase 3: NOT NULL constraint (Postgres: CHECK ... NOT VALID + VALIDATE)
ALTER TABLE users ADD CONSTRAINT users_phone_not_null
CHECK (phone IS NOT NULL) NOT VALID;
ALTER TABLE users VALIDATE CONSTRAINT users_phone_not_null;
-- ya da doğrudan ALTER COLUMN phone SET NOT NULL (daha yeni Postgres'te hızlı)
Rename kolonu
ASLA direkt rename. Kod her iki sürümüyle çalışmaz.
-- ❌ Bad
ALTER TABLE users RENAME COLUMN phone TO phone_number;
-- ✅ Good: expand-contract
-- Phase 1: yeni kolonu ekle
ALTER TABLE users ADD COLUMN phone_number TEXT;
-- Phase 2: backfill
UPDATE users SET phone_number = phone WHERE phone_number IS NULL;
-- Phase 3: uygulama her iki yerden yazsın / yeniye geç
-- (kod deploy)
-- Phase 4: eski kolonu drop
ALTER TABLE users DROP COLUMN phone;
Index ekleme
Postgres: CONCURRENTLY her zaman (bloklamaz ama transaction dışında olmalı):
CREATE INDEX CONCURRENTLY users_email_idx ON users(email);
MySQL: ALGORITHM=INPLACE, LOCK=NONE:
ALTER TABLE users ADD INDEX email_idx (email), ALGORITHM=INPLACE, LOCK=NONE;
Drop kolon / tablo
Önce kodda kullanılmaz hale getir, deploy et, bekle (1 release cycle), sonra drop.
-- Phase 1 (önce): kod kolonu yazmıyor/okumuyor
-- Phase 2 (sonra): migration
ALTER TABLE users DROP COLUMN deprecated_field;
Type değiştirme
Type değişimi = yeni kolon + backfill + contract.
-- ❌ Kötü (büyük tabloda full rewrite)
ALTER TABLE orders ALTER COLUMN total TYPE NUMERIC(12, 2);
-- ✅ İyi
-- Phase 1
ALTER TABLE orders ADD COLUMN total_new NUMERIC(12, 2);
-- Phase 2: uygulama çift-yazma moduna geçer
-- Phase 3: backfill
UPDATE orders SET total_new = total WHERE total_new IS NULL;
-- Phase 4: uygulama sadece yeniyi okur
-- Phase 5: drop old
ALTER TABLE orders DROP COLUMN total;
ALTER TABLE orders RENAME COLUMN total_new TO total;
Foreign key
Büyük tabloya FK eklerken LOCK kısa tutmak için:
-- Phase 1: NOT VALID olarak ekle (kısa LOCK)
ALTER TABLE orders ADD CONSTRAINT orders_user_fk
FOREIGN KEY (user_id) REFERENCES users(id) NOT VALID;
-- Phase 2: VALIDATE (satır satır check, LOCK light)
ALTER TABLE orders VALIDATE CONSTRAINT orders_user_fk;
Backfill — chunk'lı
Tek UPDATE ile 50M row = full table LOCK + replication lag + potential OOM.
-- ❌ Kötü
UPDATE orders SET status = 'legacy' WHERE status IS NULL;
-- ✅ İyi: batch
DO $$
DECLARE
r RECORD;
batch_size INT := 10000;
updated INT := 1;
BEGIN
WHILE updated > 0 LOOP
UPDATE orders
SET status = 'legacy'
WHERE id IN (
SELECT id FROM orders WHERE status IS NULL LIMIT batch_size
);
GET DIAGNOSTICS updated = ROW_COUNT;
PERFORM pg_sleep(0.1); -- replication'a nefes
END LOOP;
END $$;
Ya da uygulama seviyesinde cron / background job.
Rollback planı
Her migration PR'ında şu soruları yanıtla:
- Rollback gerekirse ne yaparız?
- Schema rollback kod rollback'ten önce/sonra mı?
- Data loss var mı? (DROP, TRUNCATE)
- Eski backup'tan restore süresi?
- Canlı trafik sırasında rollback güvenli mi?
Monitoring
Migration deploy edildikten sonra 1-24 saat:
- DB latency p95/p99 — yükseldi mi?
- Lock wait time — lock kuyruğu büyüyor mu?
- Replication lag — read replica'lar geride mi?
- Error rate — schema değişikliği kod tarafında yeni error getirdi mi?
- Disk usage — index rebuild / bloat?
Somut örnek — email_verified_at ekleme
İhtiyaç
users tablosuna email_verified_at TIMESTAMPTZ kolonu. NOT NULL, eski kullanıcılar için "zaten doğrulanmış kabul".
Plan
- Migration 1: kolonu nullable ekle
- Code deploy: yeni kayıtlar yazsın, eski kolonu okumayı sürdürsün (yoksa null)
- Backfill:
UPDATE users SET email_verified_at = created_at WHERE email_verified_at IS NULL(batch) - Migration 2: NOT NULL constraint
- Code deploy: artık
email_verified_at IS NULL→ henüz doğrulanmamış kabul
Migration 1
-- up
ALTER TABLE users ADD COLUMN email_verified_at TIMESTAMPTZ;
CREATE INDEX CONCURRENTLY users_email_verified_at_idx
ON users(email_verified_at) WHERE email_verified_at IS NOT NULL;
-- down
DROP INDEX IF EXISTS users_email_verified_at_idx;
ALTER TABLE users DROP COLUMN email_verified_at;
Backfill (background job)
DO $$
DECLARE updated INT := 1;
BEGIN
WHILE updated > 0 LOOP
UPDATE users SET email_verified_at = created_at
WHERE id IN (
SELECT id FROM users
WHERE email_verified_at IS NULL AND created_at < '2026-04-22'
LIMIT 10000
);
GET DIAGNOSTICS updated = ROW_COUNT;
PERFORM pg_sleep(0.1);
END LOOP;
END $$;
Migration 2
-- up
ALTER TABLE users ADD CONSTRAINT users_email_verified_not_null
CHECK (email_verified_at IS NOT NULL) NOT VALID;
ALTER TABLE users VALIDATE CONSTRAINT users_email_verified_not_null;
-- down
ALTER TABLE users DROP CONSTRAINT users_email_verified_not_null;
Anti-pattern'ler
- ❌ Kod + schema aynı deploy'da (coupled)
- ❌ Rename column direkt (her iki isim çalışmaz)
- ❌ NOT NULL + DEFAULT'u büyük tabloya direct ekleme
- ❌ Index CONCURRENTLY kullanmadan prod'a
- ❌ Down migration yazmamak
- ❌ Tek
UPDATEile 50M row güncelleme - ❌ DDL + DML aynı transaction (Postgres'te bazı DDL'ler auto-commit)
- ❌ Prod'da "deneyelim, olmazsa geri alırız" zihniyeti (staging önce)
- ❌ Schema değişikliğinden sonra monitor'a bakmamak
- ❌ 3 saat sürecek bir backfill'i peak hour'da çalıştırmak
- ❌ DROP TABLE backup olmadan
Checklist — migration PR'ı öncesi
- Up ve down migration yazıldı
- Dev DB'de up + down test edildi
- Staging'de prod-size veri ile test edildi
- Tahmini LOCK süresi kabul edilebilir
- Expand/contract pattern gerekli ise kullanıldı
- Büyük tablo için chunk'lı backfill plan
- Rollback planı yazıldı
- Deploy zamanı off-peak
- Monitoring dashboard hazır
- Code değişikliği backward compatible
- PR description'da etki analizi