
Migrasi Skema Database Tanpa Downtime: Expand and Contract dari Kasus Production yang Saya Alami
Cara saya melakukan migrasi skema database tanpa downtime di production dengan pola expand-contract, menghindari lock tabel PostgreSQL, dan rollback yang aman. Panduan praktisi backend.
Jumat malam tahun lalu, saya menambah satu kolom NOT NULL ke tabel orders yang isinya 40 juta baris. Migrasinya jalan 45 menit, dan selama 45 menit itu setiap transaksi checkout pelanggan menggantung sampai timeout. Bukan karena PostgreSQL lambat — tapi karena ALTER TABLE ... SET NOT NULL mengambil ACCESS EXCLUSIVE LOCK di tabel tersebut, dan semua query lain antre di belakangnya. Sejak itu saya tidak pernah lagi melihat migrasi database sebagai sekadar "jalankan migrate di CI". Artikel ini adalah rangkuman pola yang sekarang saya pakai untuk migrasi skema tanpa downtime, khususnya di PostgreSQL + Prisma.

Masalah Sebenarnya Bukan Downtime Deployment, Tapi Lock
Banyak tim mengira "tanpa downtime" berarti zero-downtime deployment — blue-green, rolling restart, dsb. Itu bagian dari solusi, tapi masalah yang lebih licik ada di level database: banyak operasi DDL di PostgreSQL membutuhkan lock eksklusif, dan lock itu tidak menunggu dengan sopan. Query SELECT biasa pun akan tertahan sampai DDL selesai.
Beberapa operasi yang sering menjebak:
ALTER TABLE ... ADD COLUMN ... NOT NULLtanpa default: cepat (hanya metadata), tapi tidak di semua versi dan kombinasi.ADD COLUMN ... DEFAULT xnon-volatile: sejak PostgreSQL 11 ini cuma perubahan metadata, jadi cepat. Ini pengecualian yang menguntungkan.SET NOT NULLdi kolom existing: full table scan untuk validasi. Di tabel 40 juta baris, itu 45 menit saya kemarin.- Membuat index tanpa
CONCURRENTLY: blok semua write. - Drop column, ganti tipe kolom: lock eksklusif plus rewrite tabel.
Pendekatan expand and contract — yang dibahas panjang dalam panduan zero-downtime database migrations — intinya sederhana: jangan pernah mengubah skema dan aplikasi dalam satu langkah. Pecah jadi beberapa deploy yang masing-masing kompatibel dengan skema lama maupun baru.
Pola Expand and Contract, Versi Praktis
Alurnya untuk kasus "tambah kolom wajib" seperti kasus saya:
Deploy 1 (expand): tambah kolom nullable, tanpa default.
ALTER TABLE orders ADD COLUMN payment_channel TEXT;
-- di Prisma: buat migration.sql manual, jangan generate polos
Kode aplikasi di deploy ini menulis ke kedua kolom (lama dan baru) tapi masih membaca dari kolom lama. Ini fokusnya: read path tidak berubah sama sekali.
Backfill bertahap: isi data lama bukan dengan satu UPDATE raksasa, tapi batch:
UPDATE orders
SET payment_channel = legacy_channel
WHERE payment_channel IS NULL
AND id IN (
SELECT id FROM orders
WHERE payment_channel IS NULL
LIMIT 10000
);
Saya jalankan ini via background job — pendekatan queue yang mirip dengan yang saya tulis di artikel tentang background job dan BullMQ. Batch kecil + jeda antar batch = lock row yang singkat, tidak mengganggu transaksi user. Di proyek terakhir, backfill 40 juta baris selesai dalam 3 jam tanpa ada keluhan latency.
Deploy 2 (switch read): aplikasi mulai membaca kolom baru, tapi masih menulis keduanya. Ini jendela rollback paling murah — kalau ada bug, tinggal balik ke deploy 1.
Deploy 3 (contract): baru di sini set constraint dan bersihkan:
-- PostgreSQL 12+: validasi tanpa menahan write lama
ALTER TABLE orders
ADD CONSTRAINT orders_payment_channel_not_null
CHECK (payment_channel IS NOT NULL) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT orders_payment_channel_not_null;
ALTER TABLE orders ALTER COLUMN payment_channel SET NOT NULL;
-- cek constraint NOT VALID memungkinkan SET NOT NULL skip full scan
Trik CHECK ... NOT VALID lalu VALIDATE CONSTRAINT ini yang menyelamatkan saya. VALIDATE hanya butuh SHARE UPDATE EXCLUSIVE LOCK, jadi write tetap jalan. Terakhir, hapus kolom lama — biasanya saya tunda beberapa minggu, bukan langsung, kalau-kalau ada laporan atau script lama yang masih mereferensikannya.

Alat Bantu dan Kapan Tidak Perlu Repot Sebesar Itu
Kalau tim Anda pakai Django, library django-pg-zero-downtime-migrations mengotomatiskan sebagian besar pola di atas — dia menulis ulang migrasi Django jadi varian yang aman lock. Menariknya, topik ini juga sudah diminati lama: pertanyaan klasik di Stack Overflow soal deploy aplikasi enterprise dengan perubahan skema SQL tanpa downtime sudah ada sejak 2011, dan jawabannya masih relevan: pisahkan deployment kode dari perubahan skema.
Tapi jujur: pola expand-contract itu mahal secara koordinasi — tiga deploy untuk satu kolom. Untuk tabel kecil (ratusan ribu baris, traffic rendah), SET NOT NULL langsung selesai dalam hitungan detik dan tidak akan terasa. Saya tetap pakai jalur cepat kalau tabelnya kecil dan saya yakin tidak ada long-running transaction yang sedang berjalan. Yang penting adalah tahu ukuran tabel dan pola aksesnya sebelum memutuskan.
Dengan Prisma khususnya, hati-hati: prisma migrate dev yang generate schema diff polos sering menghasilkan DDL yang tidak aman lock-nya (misal DROP COLUMN langsung). Kebiasaan saya sekarang: selalu baca migration.sql yang di-generate, edit manual bila perlu, dan jalankan lewat prisma migrate deploy yang sudah saya review.
Checklist yang Saya Pakai Sebelum Setiap Migrasi Production
Ini versi ringkas yang sekarang jadi standar di tim saya, terinspirasi setelah dua kali kena masalah lock:
- Cek
pg_stat_activityuntuk long-running transaction sebelum DDL — satu transaksi idle-in-transaction bisa membuatCREATE INDEX CONCURRENTLYmenggantung berjam-jam. - Selalu
lock_timeoutsebelum DDL berat:SET lock_timeout = '5s';— lebih baik DDL gagal dan retry daripada menggantungkan seluruh tabel. - Index selalu dengan
CREATE INDEX CONCURRENTLY, meski lambat dan tidak bisa jalan dalam satu transaksi. - Satu DDL per migration file — rollback dan analisis jadi jauh lebih mudah.
- Deploy kode dulu yang kompatibel dengan kedua versi skema, baru jalankan contract phase.
Terakhir, soal kolom yang masih dipakai foreign key atau view yang luput — PostgreSQL akan menolak drop dan itulah penyelamat Anda. Yang lebih berbahaya adalah kode aplikasi lama di worker yang belum dideploy dan masih menulis ke kolom lama. Sinkronkan versi kode semua worker sebelum contract phase, bukan sesudahnya.
Kalau saya harus merangkum satu hal: migrasi tanpa downtime itu 20% soal SQL, 80% soal urutan deploy. Kalau Anda bisa menjamin setiap versi kode kompatibel dengan dua versi skema berturut-turut, hampir semua masalah di artikel ini hilang dengan sendirinya.
Kebiasaan menulis SET lock_timeout sebelum DDL saja sudah menyelamatkan saya dari insiden dua kali. Mulai dari situ saja kalau pola lengkapnya terasa berat.
Sumber
- Zero-downtime database migrations — referensi klasik untuk pola expand and contract yang saya adaptasi di artikel ini.
- django-pg-zero-downtime-migrations — library yang mengotomatiskan migrasi Django agar aman terhadap lock PostgreSQL.
- Is it possible to deploy an enterprise ASP.NET application and SQL schema changes with zero downtime? — diskusi Stack Overflow yang menegaskan pentingnya memisahkan deployment kode dari perubahan skema.
Tomi Hartanto
Senior Software Engineer
Backend developer dengan 8+ tahun pengalaman membangun sistem scalable. Menulis tentang arsitektur backend, database, dan DevOps.
Artikel Terkait
Tomi Hartanto
Senior Software EngineerBackend developer dengan 8+ tahun pengalaman membangun sistem scalable. Menulis tentang arsitektur backend, database, dan DevOps.


