
Jul 24, 2025
DB Migrations Tanpa Drama (Two-Phase Changes)
“Cuma migration kecil kok” itu kalimat yang sering jadi opening scene buat incident. Di production, migration bukan sekadar SQL, itu operasi bedah sambil pasien masih jalan-jalan.

Maarif Alawi
Full-Stack Web App Developer
Satu ALTER TABLE bisa bikin API error, halaman blank, atau queue macet, walau code kamu “gak berubah banyak”. Dan yang bikin nyebelin: bug-nya sering muncul setengah jam setelah deploy, pas semua orang udah merasa aman. Dalam 3 tahun dan ~13 project shipped (Next.js + Supabase Postgres + Vercel + Sentry), cara paling waras adalah: two-phase changes.
Cerita singkat: missing column bikin error berantai
Pernah ada migration yang nambah kolom baru, lalu code langsung pakai kolom itu seolah semua data udah punya value. Hasilnya: endpoint tertentu meledak dengan error “column does not exist” atau value null yang gak di-handle, tergantung instance mana yang kebetulan kena duluan.
Yang bikin lucu (dan pahit): di staging aman, karena datanya baru dan bersih. Di production, ada data lama dengan format beda dan edge case yang gak kepikiran.
Sentry jadi penyelamat karena pattern error-nya jelas dan cepat kebaca. Lesson-nya: migration aman itu bukan “SQL bener”, tapi “rollout bener”.
Prinsip 3 langkah: Add → Backfill → Switch
Two-phase itu cara mikir: jangan memaksa semua berubah sekaligus. Kamu bikin perubahan yang kompatibel dengan schema lama dan baru dalam satu window rilis.
Step 1, Add: tambah kolom/tabel baru yang aman (nullable, no strict constraint dulu). Code di-deploy dalam mode “dual-read / dual-write” kalau perlu.
Step 2, Backfill: isi data lama pelan-pelan, jangan sekaligus nge-lock table. Kalau datanya besar, lakukan batch, off-peak, dan monitor.
Step 3, Switch: setelah data beres, baru ganti logic supaya pakai field baru sebagai source of truth. Baru setelah itu kamu boleh cleanup kolom lama (di rilis berikutnya), bukan di menit yang sama.
Contoh simpel: mau “rename column” full_name jadi name. Jangan rename langsung. Tambah name, backfill dari full_name, deploy code yang baca COALESCE(name, full_name), switch write ke name, baru nanti hapus full_name.
Order rilis yang bener
Urutan aman itu ngebunuh 80% drama:
Deploy code yang support dua schema (tahan null, fallback, jangan hard fail).
Jalankan migration yang additive (tambah kolom/tabel/index yang aman).
Backfill data, ideally batch + monitor (Sentry + logs).
Switch logic di code (read/write ke field baru).
Cleanup schema lama di rilis berikutnya, dengan rollback plan tetap ada.
Kalau kamu kebalik urutan (migrate dulu, code belum siap), kamu lagi main Russian roulette tapi versi database.
Anti-patterns migration yang bikin down
Rename/drop column langsung di jam kerja, lalu code lama masih jalan.
Solusinya: two-phase, keep backward compatibility dulu.Tambah
NOT NULLtanpa default/backfill.
Solusinya: tambah nullable, backfill, baru enforce constraint belakangan.Long-running migration pas traffic rame.
Solusinya: schedule off-peak, pakai batch, dan hindari table rewrite yang berat.Gak ada rollback plan, cuma “semoga aman”.
Solusinya: siapin rollback path (revert code, disable feature, atau stop switch) sebelum eksekusi.
Kode: SQL migration aman (Postgres/Supabase)
COPY THIS
Perubahan schema selalu additive dulu (no rename/drop langsung)
Gunakan pola Add → Backfill → Switch
Deploy code yang backward compatible sebelum migration besar
Field baru awalnya nullable, constraint belakangan
Backfill dilakukan batch, bukan satu tembakan nuklir
Jalankan migration berat di off-peak + monitor
Pastikan query/read punya fallback (mis. COALESCE)
Switch logic dilakukan setelah backfill selesai dan tervalidasi
Cleanup kolom lama di rilis berikutnya, bukan barengan
Rollback plan jelas (revert code/disable switch) sebelum eksekusi
Migration aman itu kebiasaan, bukan keberuntungan. Kalau kamu treat schema change sebagai rollout bertahap, production jadi boring, dan itu justru bagus.
Di dunia nyata, “boring” itu sinonim dari “reliable”.
