a.
mcp.altay.socialMCP & Prompt Katalogu
Skill
·v1.0.0·2026-04-22

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ı.

databasemigrationpostgresmysqlzero-downtimeskill

İç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:

  1. Kolonu nullable + default yok ekle (anlık)
  2. Default value yazan trigger / uygulama katmanı geçici çözüm
  3. Backfill (chunk'lı)
  4. 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:

  1. Rollback gerekirse ne yaparız?
  2. Schema rollback kod rollback'ten önce/sonra mı?
  3. Data loss var mı? (DROP, TRUNCATE)
  4. Eski backup'tan restore süresi?
  5. 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

  1. Migration 1: kolonu nullable ekle
  2. Code deploy: yeni kayıtlar yazsın, eski kolonu okumayı sürdürsün (yoksa null)
  3. Backfill: UPDATE users SET email_verified_at = created_at WHERE email_verified_at IS NULL (batch)
  4. Migration 2: NOT NULL constraint
  5. 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 UPDATE ile 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