Bir deploy, 50 milyon satırlık bir tabloda tek bir ALTER TABLE çalıştırdı ve tabloyu dört dakika kilitledi. Dört dakika boyunca o tabloya değen her istek bekledi; site fiilen çöktü. Migration’ın kendisi doğruydu — sorun, onun tek adımda yapılmasıydı.

Tehlikeli olan şema değişikliği değildir. Tehlikeli olan, şema değişikliğini tek seferde, geri uyumluluğu düşünmeden yapmaktır.

Lock’u kim alır

Her şema değişikliği aynı maliyette değildir. Modern PostgreSQL’de sabit DEFAULT ile kolon eklemek bir metadata işlemidir — hızlıdır. Asıl tehlike, tabloyu uzun süre kilitleyen işlemlerdedir:

  • CREATE INDEXCONCURRENTLY olmadan tabloyu yazmaya kapatır.
  • Tabloyu yeniden yazan ALTER COLUMN ... TYPE değişiklikleri.
  • Tüm tabloyu tarayan NOT NULL, CHECK veya foreign key eklemeleri.

Bu işlemler güçlü bir lock alır; ACCESS EXCLUSIVE alanlar — tablo yeniden yazımı ya da NOT NULL/CHECK eklemesi — tabloya değen her sorguyu, okuma dahil, kuyruğa sokar. Geri kalanı yalnızca yazmaları bloklar. Tablo büyükse kuyruk uzar.

Tehlikeli olan tek adımdır

Çözüm migration yapmamak değil; her tehlikeli migration’ı, her biri tek başına güvenli ve geri uyumlu olan küçük adımlara bölmektir. Bunun adı expand-contract (genişlet–daralt) kalıbıdır.

Bir kolonu yeniden adlandırmak istediğinizi düşünün. Tek adımda RENAME COLUMN, eski kolonu okuyan çalışan kodu anında kırar. Bunun yerine üç deploy:

  1. Genişlet. Yeni kolonu ekle. Kod hem eskiye hem yeniye yazsın, hâlâ eskiden okusun.
  2. Geçiş. Eski veriyi yeni kolona batch’ler hâlinde taşı. Kod artık yeniden okusun.
  3. Daralt. Eski kolonu sil.

Her adım, hem bir önceki sürümdeki kod hem de yeni kod ile çalışır. Hiçbir an, çalışan kod ile şema uyumsuz değildir.

Güvenli reçeteler

Sık yapılan değişikliklerin kesintisiz hâlleri:

Yeni NOT NULL kolon. Tek adımda NOT NULL eklemek tabloyu tarar. Bölün:

-- 1. Önce nullable ekle
ALTER TABLE orders ADD COLUMN status text;

-- 2. Mevcut satırları batch'ler hâlinde doldur (uygulama tarafında)

-- 3. Kısıtı önce doğrulamadan ekle, sonra ayrı adımda doğrula
ALTER TABLE orders ADD CONSTRAINT orders_status_not_null
    CHECK (status IS NOT NULL) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT orders_status_not_null;

NOT VALID ile eklenen kısıt yeni satırlar için hemen geçerlidir ama mevcut satırları taramaz; VALIDATE ise tabloyu yalnızca SHARE UPDATE EXCLUSIVE lock ile tarar — yazmaları engellemez.

İndeks. Her zaman CONCURRENTLY:

CREATE INDEX CONCURRENTLY idx_orders_status ON orders (status);

Foreign key. Aynı iki adımlı kalıp: önce ADD CONSTRAINT ... NOT VALID, sonra VALIDATE CONSTRAINT.

lock_timeout. Bir migration, uzun süren bir sorgunun arkasında lock için beklerken kendisi de arkasındaki her şeyi bloklar. Bunu önlemek için migration oturumuna kısa bir lock_timeout verin — lock hemen alınamıyorsa migration beklemek yerine başarısız olsun, siz tekrar deneyin.

Kod ve şema birlikte uyumlu olmalı

Expand-contract’ın özü tek bir kuralda: her ara adımda, hem çalışmakta olan eski kod hem de yeni kod mevcut şema ile çalışabilmeli.

Veritabanı şeması, ileride lazım olur yazısında “tek yönlü kapı” dediğim şeydir — geri almak pahalıdır. Bu yüzden değişikliği büyük ve geri dönülmez tek bir adım olarak değil, her biri ayrı ayrı geri alınabilir küçük adımlar olarak tasarlayın.


Sıfır kesintili migration bir araç değil, bir disiplindir: her şema değişikliğini, çalışan kodun hiç fark etmeyeceği kadar küçük adımlara bölmek.

Tehlikeli olan değişikliğin kendisi değil, onu tek nefeste yapmaktır.