Ketika Migrasi Database Gagal di Production: Tiga Skenario yang Akan Membuat Anda Begadang
Anda baru saja menjalankan migrasi di production. Berhasil. Tidak ada error, tidak ada timeout, tidak ada tabel yang terkunci. Anda lega dan melanjutkan ke tugas berikutnya.
Dua jam kemudian, ponsel Anda berdering. Sebuah laporan rusak. Data terlihat salah. Sebuah service yang Anda lupakan sedang menulis NULL ke tabel-tabel kritis. Migrasi berhasil, tapi sistem production Anda hancur berantakan.
Inilah mimpi buruk migrasi database. Tidak seperti deployment aplikasi di mana kegagalan biasanya langsung terlihat dan jelas, kegagalan migrasi bisa bersembunyi selama berjam-jam atau berhari-hari. Saat Anda menyadarinya, kerusakan sudah menyebar.
Mari saya tunjukkan tiga skenario nyata di mana migrasi menjadi kacau, bukan karena SQL-nya gagal, tapi karena efek sampingnya mengejutkan semua orang.
Skenario Satu: Kolom Baru yang Menghancurkan Segalanya
Tim Anda perlu menambahkan kolom phone_number ke tabel users. Migrasi berjalan mulus di staging. Semua tes lolos. Anda push ke production dengan percaya diri.
Kolom berhasil dibuat. Tidak ada error. Tapi beberapa detik kemudian, aplikasi mulai berperilaku aneh.
Inilah yang terjadi: aplikasi production belum diupdate. Kode lama masih berjalan, dan ia mengirim query seperti SELECT * FROM users. Itu tidak masalah — kolom baru hanya diabaikan. Masalah sebenarnya ada di tempat lain. Potongan kode lain mulai memasukkan data ke phone_number, tapi menggunakan format yang berbeda dari yang diharapkan aplikasi baru. Nomor telepon masuk dalam format campuran — ada yang dengan kode negara, ada yang tanpa, ada yang dengan tanda strip, ada yang tanpa.
Perhatikan migrasi yang memicu skenario ini:
ALTER TABLE users ADD COLUMN phone_number VARCHAR(20);
Ini terlihat tidak berbahaya. Tapi tanpa constraint NOT NULL atau nilai default, kolom ini menerima format apa pun. Lebih parah lagi, jika tabelnya besar, ALTER TABLE ini akan mengunci tabel untuk operasi tulis selama seluruh proses. Di production, lock ini bisa mengantrekan ratusan request dalam hitungan detik. Bahaya sebenarnya bukanlah SQL-nya sendiri — melainkan bahwa skema berubah sebelum semua instance aplikasi siap menanganinya.
Sekarang Anda memiliki data yang tidak konsisten di sebuah kolom yang akan diandalkan oleh banyak sistem. Tim Anda dihadapkan pada pilihan yang tidak mengenakkan: mencoba membersihkan data yang ada, atau terburu-buru mendorong versi aplikasi baru ke production sebelum siap.
Inti masalahnya di sini adalah waktu. Skema berubah sebelum kode aplikasi yang memahaminya selesai di-deploy. Dalam sistem terdistribusi, tidak semua instance diperbarui pada saat yang bersamaan. Untuk waktu yang singkat — kadang lebih lama — kode lama berinteraksi dengan skema baru.
Skenario Dua: Perubahan Tipe yang Merusak Laporan Malam Hari
Ini lebih halus. Tim Anda memutuskan untuk mengubah kolom price dari integer menjadi decimal. Ide bagus — harga butuh presisi. Migrasi berjalan sempurna. Tidak ada error langsung. Aplikasi tampak baik-baik saja.
Tapi enam bulan lalu, seseorang menulis query laporan yang memperlakukan price sebagai integer. Query itu tidak digunakan di halaman utama. Query itu berjalan sekali semalam untuk laporan keuangan. Pada jam 2 pagi, laporan gagal total. Setiap query yang membandingkan harga dengan nilai integer sekarang melempar error ketidakcocokan tipe.
Inilah yang disebut engineer sebagai blocking change. Perubahan skema tidak merusak apa pun yang terlihat di siang hari, tapi diam-diam merusak proses batch kritis yang berjalan di malam hari. Di pagi hari, tim keuangan bertanya mengapa angka kemarin tidak cocok.
Bagian berbahayanya? Anda mungkin tidak menemukan kegagalan ini selama berjam-jam. Dan perbaikannya tidak sederhana. Anda tidak bisa begitu saja mengembalikan perubahan tipe tanpa migrasi lain, yang memiliki risikonya sendiri.
Skenario Tiga: Kolom yang Dihapus Meracuni Tiga Tabel
Ini adalah skenario paling berbahaya. Tim Anda yakin bahwa kolom old_status sudah tidak digunakan lagi. Kolom itu sudah didepresiasi berbulan-bulan lalu. Tidak ada yang mereferensikannya di aplikasi utama. Anda menulis migrasi untuk menghapusnya.
Migrasi berjalan mulus. Kolom menghilang. Tidak ada error di mana pun.
Tapi ada sebuah service latar belakang — sebuah job sinkronisasi data yang ditulis oleh tim yang sudah keluar dari perusahaan dua tahun lalu — yang masih membaca old_status secara periodik. Service itu tidak crash saat kolom hilang. Ia hanya mulai menulis nilai NULL ke tabel-tabel lain. Nilai null menyebar. Integritas data rusak secara diam-diam di tiga tabel berbeda selama dua jam berikutnya.
Saat seseorang menyadarinya, kerusakan sudah terjadi. Anda tidak bisa begitu saja "membatalkan" penghapusan kolom. Data di tabel-tabel lain itu sudah korup. Pemulihan membutuhkan pemahaman yang tepat tentang baris mana yang terpengaruh, merekonstruksi nilai yang hilang dari backup, dan menjalankan script perbaikan yang hati-hati.
Mengapa Migrasi Database Berbeda dari Deployment Aplikasi
Ketiga skenario ini memiliki pola yang sama: migrasi dieksekusi dengan sukses, tapi efek sampingnya muncul belakangan. Inilah yang membuat migrasi database fundamentally berbeda dari deployment aplikasi.
Ketiga skenario di atas memiliki pola yang jelas: perubahan skema yang sukses memicu kegagalan yang tertunda. Diagram alir berikut memetakan setiap skenario dari akar penyebab hingga konsekuensi.
Ketika deployment aplikasi gagal, Anda biasanya langsung tahu. Error muncul di log. Pengguna melaporkan masalah. Alert monitoring berbunyi. Anda bisa roll back versi aplikasi dan mengembalikan layanan dengan cepat.
Migrasi database tidak bekerja seperti itu. Perubahan skema bisa:
- Menciptakan inkonsistensi yang baru muncul saat data baru datang
- Merusak query yang berjalan sesuai jadwal, bukan terus-menerus
- Menyebabkan korupsi data yang menyebar perlahan ke tabel-tabel terkait
- Mempengaruhi service yang Anda lupa ada atau tidak Anda ketahui
Bagian terburuknya? Setelah kerusakan terjadi, Anda tidak bisa begitu saja "membatalkan" perubahan skema seperti Anda mengembalikan perubahan kode. Kolom yang dihapus tidak bisa dikembalikan dengan mudah, terutama jika tabel lain sudah bergantung pada ketiadaannya. Tipe data yang diubah membutuhkan migrasi balik yang memiliki risikonya sendiri.
Checklist Praktis Sebelum Migrasi Production Berikutnya
Sebelum Anda menjalankan migrasi berikutnya di production, lakukan pemeriksaan ini:
- Identifikasi semua konsumen. Daftar setiap service, cron job, laporan, dan pipeline data yang menyentuh tabel yang terpengaruh. Jangan berasumsi Anda tahu semuanya.
- Periksa eksekusi yang ditunda. Temukan query yang berjalan sesuai jadwal, proses batch, atau job latar belakang. Inilah yang akan gagal secara diam-diam berjam-jam kemudian.
- Verifikasi backward compatibility. Apakah kode aplikasi lama masih bisa bekerja dengan skema baru? Setidaknya untuk satu siklus deployment, skema Anda harus mendukung kode lama dan baru.
- Siapkan rencana pemulihan. Ketahui persis bagaimana Anda akan mengembalikan data jika terjadi kesalahan. Uji proses pemulihan, bukan hanya migrasinya.
- Jalankan migrasi saat traffic rendah. Bahkan dengan semua tindakan pencegahan, beri diri Anda buffer untuk menangkap masalah sebelum mempengaruhi pengguna.
Kesimpulan Konkret
Migrasi yang sukses bukanlah migrasi yang berjalan tanpa error. Migrasi yang sukses adalah migrasi yang tidak merusak apa pun — sekarang, satu jam lagi, atau jam 3 pagi saat laporan malam berjalan. Perlakukan setiap perubahan skema sebagai bom waktu potensial, dan verifikasi bahwa semua sistem, bukan hanya yang jelas-jelas terlihat, bisa menangani struktur baru sebelum Anda menganggap migrasi selesai.