Pelajari cara melakukan perubahan skema tanpa downtime dengan pola expand/contract: tambahkan kolom dengan aman, backfill bertahap, deploy kode kompatibel, lalu hapus jalur lama.

Downtime dari perubahan basis data tidak selalu berupa pemadaman yang bersih dan jelas. Bagi pengguna bisa terlihat seperti halaman yang memuat selamanya, checkout yang gagal, atau aplikasi yang tiba-tiba menampilkan "ada yang salah." Bagi tim, ini muncul sebagai alert, kenaikan tingkat error, dan antrean penulisan yang gagal yang perlu dibersihkan.
Perubahan skema berisiko karena basis data dipakai bersama oleh setiap versi aplikasi yang berjalan. Saat merilis seringkali Anda memiliki kode lama dan baru hidup bersamaan (rolling deploy, banyak instance, job background). Migrasi yang tampak benar sekalipun bisa memecah salah satu versi tersebut.
Mode kegagalan umum termasuk:
Bahkan ketika kode sudah benar, rilis bisa terhambat karena masalah sebenarnya adalah waktu dan kompatibilitas antar versi. Perubahan skema tanpa downtime berpegang pada satu aturan: setiap status menengah harus aman untuk kode lama dan baru. Anda mengubah basis data tanpa memecah baca dan tulis yang ada, mengirim kode yang bisa menangani kedua bentuk, dan hanya menghapus jalur lama setelah tak ada yang bergantung padanya.
Upaya ekstra ini sepadan ketika Anda punya lalu lintas nyata, SLA ketat, atau banyak instance dan worker. Untuk alat internal kecil dengan basis data sepi, jendela maintenance terencana bisa lebih sederhana.
Kebanyakan insiden dari pekerjaan basis data terjadi karena aplikasi mengharapkan basis data berubah seketika, sementara perubahan basis data butuh waktu. Pola expand/contract menghindari itu dengan memecah satu perubahan berisiko menjadi langkah-langkah kecil yang aman.
Untuk sementara, sistem Anda mendukung dua "dialek" sekaligus. Anda perkenalkan struktur baru dulu, biarkan yang lama tetap bekerja, pindahkan data secara bertahap, lalu bersihkan.
Polanya sederhana:
Ini cocok dengan rolling deploy. Jika Anda memperbarui 10 server satu per satu, Anda akan sementara menjalankan versi lama dan baru bersama-sama. Expand/contract menjaga keduanya kompatibel dengan basis data yang sama selama overlap itu.
Pola ini juga membuat rollback jadi kurang menakutkan. Jika rilis baru punya bug, Anda bisa rollback aplikasi tanpa rollback basis data, karena struktur lama masih ada selama jendela expand.
Contoh: Anda ingin membagi kolom PostgreSQL full_name menjadi first_name dan last_name. Anda tambahkan kolom baru (expand), kirim kode yang bisa menulis dan membaca kedua bentuk, backfill baris lama, lalu drop full_name setelah yakin tak ada yang menggunakannya (contract).
Fase expand tentang menambah opsi baru, bukan menghapus yang lama.
Langkah umum pertama adalah menambah kolom baru. Di PostgreSQL, biasanya paling aman menambahkannya sebagai nullable dan tanpa default. Menambah kolom non-null dengan default bisa memicu penulisan ulang tabel atau penguncian lebih berat, tergantung versi Postgres dan perubahan tepatnya. Urutan yang lebih aman: tambah nullable, deploy kode toleran, backfill, lalu kemudian tegakkan NOT NULL.
Indeks juga perlu perhatian. Membuat indeks biasa bisa memblokir penulisan lebih lama dari yang Anda kira. Bila memungkinkan, gunakan pembuatan indeks secara concurrent agar baca dan tulis tetap berjalan. Ini memakan waktu lebih lama, tapi menghindari lock yang menghentikan rilis.
Expand juga bisa berarti menambah tabel baru. Jika Anda bergerak dari satu kolom ke relasi many-to-many, Anda mungkin menambahkan tabel join sambil mempertahankan kolom lama. Jalur lama tetap bekerja sementara struktur baru mulai mengumpulkan data.
Dalam praktik, expand sering mencakup:
Setelah expand, versi aplikasi lama dan baru seharusnya bisa berjalan bersamaan tanpa kejutan.
Sebagian besar masalah rilis terjadi di tengah: beberapa server menjalankan kode baru, yang lain masih menjalankan kode lama, sementara basis data sudah berubah. Tujuan Anda sederhana: setiap versi saat rollout harus bekerja dengan skema lama dan yang sudah di-expand.
Pendekatan umum adalah dual-write. Jika Anda menambah kolom baru, aplikasi baru menulis ke kolom lama dan kolom baru. Versi lama tetap menulis hanya ke kolom lama, yang aman karena kolom itu masih ada. Biarkan kolom baru bersifat opsional pada awalnya, dan tunda penegakan constraint ketat sampai semua penulis ter-upgrade.
Pembacaan biasanya beralih lebih hati-hati daripada penulisan. Untuk sementara, biarkan pembacaan pada kolom lama (yang Anda tahu sudah terisi penuh). Setelah backfill dan verifikasi, ubah pembacaan untuk memprioritaskan kolom baru, dengan fallback ke yang lama bila yang baru kosong.
Juga jaga agar output API stabil selama perubahan basis data. Bahkan jika Anda memperkenalkan field internal baru, hindari mengubah bentuk respons sampai semua konsumen siap (web, mobile, integrasi).
Rollout yang ramah rollback sering terlihat seperti ini:
Ide kuncinya adalah langkah yang pertama kali tak bisa dibalik adalah menghapus struktur lama, jadi tunda sampai akhir.
Backfill adalah titik di mana banyak "perubahan skema tanpa downtime" gagal. Anda ingin mengisi kolom baru untuk baris yang ada tanpa lock lama, kueri lambat, atau lonjakan beban tak terduga.
Batching penting. Usahakan batch yang selesai cepat (detik, bukan menit). Jika setiap batch kecil, Anda bisa jeda, lanjutkan, dan menyetel job tanpa memblokir rilis.
Untuk melacak progres, gunakan cursor yang stabil. Di PostgreSQL itu sering kunci primer. Proses baris berurutan dan simpan id terakhir yang selesai, atau kerjakan rentang id. Ini menghindari full-table scan mahal saat job restart.
Berikut pola sederhana:
UPDATE my_table
SET new_col = ...
WHERE new_col IS NULL
AND id > $last_id
ORDER BY id
LIMIT 1000;
Buat update bersyarat (mis. WHERE new_col IS NULL) sehingga job idempoten. Rerun hanya menyentuh baris yang masih perlu, yang mengurangi penulisan yang tidak perlu.
Rencanakan data baru yang datang selama backfill. Urutan yang biasa:
Backfill yang baik itu membosankan: stabil, terukur, dan mudah dijeda jika database mulai panas.
Momen paling berisiko bukan menambah kolom baru. Melainkan memutuskan Anda bisa mengandalkannya.
Sebelum pindah ke contract, buktikan dua hal: data baru lengkap, dan produksi telah membacanya dengan aman.
Mulai dengan pemeriksaan kelengkapan yang cepat dan dapat diulang:
Jika Anda dual-writing, tambahkan pemeriksaan konsistensi untuk menangkap bug senyap. Misalnya, jalankan kueri setiap jam yang menemukan baris di mana old_value <> new_value dan alert jika tidak nol. Ini sering cara tercepat menemukan bahwa satu penulis masih hanya mengupdate kolom lama.
Pantau sinyal produksi dasar saat migrasi berjalan. Jika waktu kueri atau lock waits melonjak, bahkan kueri verifikasi Anda yang "aman" bisa menambah beban. Pantau tingkat error untuk jalur kode yang membaca kolom baru, terutama segera setelah deploy.
Berapa lama menyimpan kedua jalur? Cukup lama untuk melewati setidaknya satu siklus rilis penuh dan satu rerun backfill. Banyak tim memakai 1–2 minggu, atau sampai yakin tidak ada versi aplikasi lama yang masih berjalan.
Contract adalah saat tim sering gugup karena terasa seperti titik tanpa kembali. Jika expand dilakukan dengan benar, contract sebagian besar adalah pembersihan, dan Anda masih bisa melakukannya dalam langkah kecil berisiko rendah.
Pilih waktu dengan hati-hati. Jangan drop apa pun tepat setelah backfill selesai. Beri setidaknya satu siklus rilis penuh sehingga job tertunda dan kasus tepi punya waktu muncul.
Urutan contract yang aman biasanya:
Jika bisa, bagi contract ke dua rilis: satu yang menghapus referensi kode (dengan logging ekstra), dan yang lain yang menghapus objek basis data. Pemisahan itu memudahkan rollback dan troubleshooting.
Spesifik PostgreSQL penting di sini. Drop kolom sebagian besar adalah perubahan metadata, tapi tetap mengambil lock ACCESS EXCLUSIVE sebentar. Rencanakan saat tenang dan buat migrasi cepat. Jika Anda menambah indeks ekstra, pilih DROP INDEX CONCURRENTLY untuk menghindari memblokir penulisan (itu tidak bisa dijalankan dalam transaction block, jadi tooling migrasi Anda harus mendukungnya).
Migrasi tanpa downtime gagal ketika basis data dan aplikasi berhenti sepakat tentang apa yang diperbolehkan. Pola ini bekerja hanya jika setiap status menengah aman untuk kode lama dan baru.
Kesalahan ini sering muncul:
Skenario realistis: Anda mulai menulis full_name dari API, tapi job background yang membuat user masih hanya mengisi first_name dan last_name. Job itu berjalan malam hari, memasukkan baris dengan full_name = NULL, dan kemudian kode lain mengasumsikan full_name selalu ada.
Anggap setiap langkah seperti rilis yang dapat berlangsung berhari-hari:
Checklist yang bisa diulang menjaga Anda dari mengirim kode yang hanya bekerja di satu status basis data.
Sebelum deploy, konfirmasi basis data sudah memiliki bagian expand (kolom/tabel baru, indeks dibuat dengan cara low-lock). Lalu pastikan aplikasi toleran: harus bekerja terhadap bentuk lama, bentuk yang di-expand, dan kondisi setengah-backfilled.
Jaga checklist singkat:
Sebuah migrasi dianggap selesai ketika baca menggunakan data baru, tulis tidak lagi mempertahankan data lama, dan Anda sudah memverifikasi backfill dengan setidaknya satu cek sederhana (count atau sampling).
Misalnya Anda punya tabel PostgreSQL customers dengan kolom phone yang menyimpan nilai berantakan (berbagai format, kadang kosong). Anda ingin menggantinya dengan phone_e164, tapi tidak bisa memblokir rilis atau menurunkan aplikasi.
Urutan expand/contract yang bersih terlihat seperti ini:
phone_e164 sebagai nullable, tanpa default, dan tanpa constraint berat dulu.phone dan phone_e164, tapi tetap membaca dari phone supaya pengguna tidak melihat perubahan.phone_e164 terlebih dahulu, dan fallback ke phone jika masih NULL.phone_e164, hapus fallback, drop phone, lalu tambahkan constraint lebih ketat jika masih diperlukan.Rollback tetap sederhana ketika setiap langkah kompatibel ke belakang. Jika switch baca menyebabkan masalah, rollback aplikasi dan basis data masih punya kedua kolom. Jika backfill menyebabkan lonjakan beban, jeda job, kecilkan ukuran batch, dan lanjutkan nanti.
Untuk menjaga tim tetap selaras, dokumentasikan rencana di satu tempat: SQL tepatnya, rilis mana yang mengubah pembacaan, bagaimana mengukur penyelesaian (mis. persentase non-NULL phone_e164), dan siapa pemilik setiap langkah.
Expand/contract bekerja terbaik ketika terasa rutin. Tulis runbook singkat yang bisa dipakai ulang oleh tim Anda setiap kali melakukan perubahan skema, idealnya satu halaman dan cukup spesifik sehingga rekan baru bisa mengikutinya.
Template praktis mencakup:
Tentukan kepemilikan di muka. "Semua pikir orang lain yang akan melakukan contract" adalah alasan kolom lama dan feature flag hidup berbulan-bulan.
Bahkan jika backfill berjalan online, jadwalkan saat lalu lintas lebih rendah. Lebih mudah menjaga batch kecil, memantau beban DB, dan berhenti cepat jika latensi naik.
Jika Anda membangun dan deploy dengan Koder.ai (koder.ai), Planning Mode bisa menjadi cara berguna untuk memetakan fase dan checkpoint sebelum menyentuh produksi. Aturan kompatibilitas yang sama tetap berlaku, tetapi menuliskan langkah membuatnya lebih sulit untuk melewatkan bagian membosankan yang mencegah gangguan.
Karena basis data Anda dipakai bersama oleh semua versi aplikasi yang berjalan. Selama rolling deploy dan job background, kode lama dan baru bisa berjalan bersamaan, dan migrasi yang mengubah nama, menghapus kolom, atau menambah constraint bisa memecah versi mana pun yang tidak dibuat untuk status skema itu.
Artinya Anda merancang migrasi sehingga setiap status sementara basis data aman untuk kode lama dan baru. Anda menambahkan struktur baru lebih dulu, menjalankan kedua jalur (lama dan baru) untuk sementara, lalu menghapus struktur lama hanya setelah tidak ada yang bergantung padanya.
Expand menambahkan kolom, tabel, atau indeks baru tanpa menghapus apa pun yang dibutuhkan aplikasi saat ini. Contract adalah fase pembersihan di mana Anda menghapus kolom lama, baca/tulis lama, dan logika sinkronisasi sementara setelah jalur baru terbukti bekerja sepenuhnya.
Menambahkan kolom nullable tanpa default biasanya langkah paling aman karena menghindari penguncian berat dan menjaga kode lama tetap bekerja. Setelah itu deploy kode yang tahan terhadap kolom yang hilang atau NULL, backfill bertahap, dan baru setelahnya ketatkan constraint seperti NOT NULL.
Gunakan ketika versi aplikasi baru menulis ke field baru sementara versi lama masih menulis ke field lama. Versi baru menulis ke keduanya sehingga data tetap konsisten saat masih ada instance lama dan job yang hanya mengenal field lama.
Lakukan backfill dalam batch kecil yang selesai cepat, dan buat setiap batch idempoten supaya rerun hanya memperbarui baris yang masih perlu. Pantau waktu kueri, lock waits, dan lag replikasi, dan siap untuk menjeda atau memperkecil ukuran batch jika database mulai panas.
Periksa kelengkapan, misalnya berapa baris yang masih NULL di kolom baru. Lakukan pengecekan konsistensi yang membandingkan nilai lama dan baru pada sampel (atau secara terus-menerus jika murah), dan pantau error produksi setelah deploy untuk mendeteksi jalur kode yang masih menggunakan skema lama.
NOT NULL atau constraint baru dapat memblokir penulisan saat tabel divalidasi, dan pembuatan indeks biasa bisa memegang lock lebih lama dari perkiraan. Rename dan drop juga berisiko karena kode lama mungkin masih mereferensi nama lama selama rolling deploy.
Hanya setelah Anda berhenti menulis ke field lama, mengalihkan pembacaan ke field baru tanpa fallback, dan menunggu cukup lama untuk yakin tidak ada versi aplikasi lama atau worker yang masih berjalan. Banyak tim memisahkan ini sebagai rilis terpisah sehingga rollback tetap sederhana.
Jika Anda bisa menerima jendela maintenance dan lalu lintas rendah, migrasi sekali jalan mungkin cukup. Jika Anda punya pengguna nyata, banyak instance, worker background, atau SLA, expand/contract biasanya sepadan karena menjaga rollout dan rollback lebih aman; di Koder.ai Planning Mode, menuliskan fase dan cek poin sebelumnya membantu menghindari melewatkan langkah “membosankan” yang mencegah gangguan.