Mengganti Nama Kolom, Memisahkan Tabel, dan Mengubah Constraint Tanpa Downtime
Anda memiliki tabel users dengan kolom bernama full_name. Tim memutuskan bahwa kolom tersebut harus diganti menjadi display_name. Jika Anda langsung mengganti namanya, setiap aplikasi yang masih membaca full_name akan rusak begitu perubahan masuk ke produksi. Kolomnya hilang, query gagal, dan pengguna melihat error.
Ini bukan masalah hipotetis. Tim mengganti nama kolom, memisahkan tabel menjadi dua, dan mengubah constraint setiap sprint. Pendekatan naif — mengubah skema dan memperbaiki kode setelahnya — menyebabkan insiden produksi yang sebenarnya bisa dihindari. Solusinya adalah pola yang disebut expand-contract, dan pola ini berlaku untuk ketiga skenario tersebut.
Ide Inti: Tambah Dulu, Alihkan Bertahap, Hapus Terakhir
Pola expand-contract memiliki tiga fase. Pertama, Anda memperluas skema dengan menambahkan struktur baru di samping struktur lama. Kemudian Anda memigrasikan aplikasi dan data untuk menggunakan struktur baru. Terakhir, Anda melakukan kontraksi dengan menghapus struktur lama setelah tidak ada lagi yang bergantung padanya.
Diagram di bawah mengilustrasikan pola expand-contract tiga fase dan bagaimana penerapannya untuk mengganti nama kolom, memisahkan tabel, dan mengubah constraint.
Wawasan utamanya adalah Anda tidak pernah melakukan perubahan yang memutuskan dalam satu langkah. Anda selalu menjaga jalur lama tetap berfungsi hingga jalur baru diadopsi sepenuhnya. Ini berarti zero downtime selama perubahan skema, selama Anda mengikuti urutan dengan benar.
Mengganti Nama Kolom Tanpa Merusak Apa Pun
Mari kita bahas penggantian nama dari full_name ke display_name. Pada fase expand, Anda menambahkan kolom baru display_name ke tabel users. Anda tidak menghapus full_name. Versi baru aplikasi Anda mulai menulis ke kedua kolom. Setiap insert atau update menulis nilai yang sama ke full_name untuk konsumen lama dan ke display_name untuk konsumen baru.
Berikut adalah perintah SQL untuk setiap fase penggantian nama:
-- Fase 1: Expand - tambah kolom baru
ALTER TABLE users ADD COLUMN display_name VARCHAR(255);
-- Contoh dual-write (logika aplikasi, bukan hanya SQL)
-- Saat insert atau update user, tulis ke kedua kolom:
INSERT INTO users (full_name, display_name) VALUES ('Alice', 'Alice');
UPDATE users SET full_name = 'Bob', display_name = 'Bob' WHERE id = 42;
-- Fase 2: Migrate - backfill data yang sudah ada
UPDATE users SET display_name = full_name WHERE display_name IS NULL;
-- Fase 3: Contract - hapus kolom lama
ALTER TABLE users DROP COLUMN full_name;
Urutan ini memastikan bahwa tidak ada satu pun titik di mana database menolak query atau kehilangan data.
Setelah kolom ada dan aplikasi menulis ke keduanya, Anda menjalankan backfill. Ini adalah proses batch yang menyalin semua nilai yang ada dari full_name ke display_name untuk setiap baris. Anda memverifikasi jumlahnya cocok dan memeriksa sampel acak untuk memastikan tidak ada yang hilang.
Sekarang tibalah peralihan. Semua aplikasi yang membaca nama pengguna harus diperbarui untuk membaca dari display_name alih-alih full_name. Ini bisa dilakukan secara bertahap. Beberapa layanan beralih terlebih dahulu, yang lain menyusul. Selama periode ini, kedua kolom tetap terisi, sehingga layanan apa pun yang masih membaca full_name tetap berfungsi.
Setelah setiap aplikasi dan setiap query dimigrasi, Anda memasuki fase contract. Anda menghapus kolom full_name. Seluruh proses memakan waktu berhari-hari atau berminggu-minggu tergantung pada berapa banyak layanan yang perlu diperbarui, tetapi tidak pernah ada momen di mana pengguna tidak dapat mengakses aplikasi.
Memisahkan Satu Tabel Menjadi Dua
Skenario ini lebih kompleks. Bayangkan tabel orders Anda menyimpan detail pesanan dan informasi pembayaran dalam baris yang sama. Tim ingin memisahkan data pembayaran ke dalam tabel payments khusus. Anda tidak bisa begitu saja membuat tabel baru dan berhenti menulis ke tabel lama, karena aplikasi yang ada masih membaca dari orders.
Fase expand membuat tabel payments. Versi baru aplikasi mulai menulis data pembayaran ke kedua tempat. Setiap kali pesanan dibuat atau diperbarui, aplikasi menulis detail pembayaran ke tabel orders untuk konsumen lama dan ke tabel payments untuk struktur baru. Ini disebut dual-write, dan ini adalah bagian tersulit untuk dilakukan dengan benar. Kedua operasi tulis harus berhasil atau keduanya harus di-rollback. Penulisan parsial akan merusak data Anda.
Backfill sangat penting di sini. Anda perlu menyalin semua data pembayaran yang ada dari orders ke payments. Jalankan dalam batch untuk menghindari penguncian tabel terlalu lama. Setelah setiap batch, verifikasi bahwa jumlah record dan total jumlah pembayaran cocok antara kedua tabel. Jika tidak cocok, hentikan dan selidiki sebelum melanjutkan.
Setelah backfill diverifikasi dan semua aplikasi diperbarui untuk membaca data pembayaran dari payments alih-alih orders, Anda memasuki fase contract. Anda menghapus kolom pembayaran dari orders. Langkah ini membutuhkan keyakinan bahwa tidak ada query, laporan, atau layanan lama yang masih mengakses kolom tersebut. Periksa log database, log aplikasi, dan monitoring query Anda sebelum menghapus apa pun.
Mengubah Constraint Nullable Menjadi Not Null
Skenario ini terlihat sederhana tetapi sering mengecoh tim. Tabel users Anda memiliki kolom email yang mengizinkan nilai null. Bisnis sekarang mengharuskan setiap pengguna memiliki email. Jika Anda langsung mengubah kolom menjadi NOT NULL, database akan menolak perubahan karena baris yang ada dengan email null melanggar constraint.
Fase expand di sini tidak menambahkan kolom baru dalam arti tradisional. Pendekatan umum adalah menambahkan kolom baru email_not_null yang mencerminkan email tetapi dengan constraint not-null. Versi aplikasi baru menulis ke kedua kolom. Untuk insert, kedua kolom mendapatkan nilai yang sama. Untuk update, keduanya diperbarui.
Backfill adalah langkah penentu. Setiap baris dengan email null harus diperbaiki. Anda perlu memberikan nilai default, berkoordinasi dengan pengguna untuk mengisi email mereka, atau bekerja sama dengan tim lain untuk menyediakan data yang hilang. Ini bukan masalah teknis saja. Ini adalah masalah kualitas data dan organisasi. Skrip backfill harus mencatat setiap baris yang tidak dapat diperbaiki dan memberi tahu tim untuk menangani kasus tersebut secara manual.
Setelah semua baris memiliki email yang valid dan semua aplikasi membaca dari email_not_null, Anda melakukan kontraksi dengan menghapus kolom email lama. Jika Anda ingin mempertahankan nama kolom asli, Anda dapat mengganti nama email_not_null menjadi email setelah kolom lama dihapus.
Daftar Periksa Praktis untuk Perubahan Skema
Sebelum Anda menjalankan migrasi skema apa pun di produksi, periksa daftar ini:
- Dapatkah skema lama masih melayani permintaan setelah perubahan?
- Apakah ada jalur dual-write untuk semua data baru?
- Apakah skrip backfill sudah diuji pada salinan data produksi?
- Sudahkah Anda memverifikasi bahwa semua konsumen telah dimigrasi sebelum menghapus apa pun?
- Apakah Anda memiliki rencana rollback jika fase contract mengungkapkan dependensi yang terlewat?
Kesimpulan
Setiap perubahan skema yang Anda lakukan di produksi harus mengikuti urutan yang sama: tambahkan struktur baru, migrasikan data dan aplikasi secara bertahap, dan hapus struktur lama hanya ketika tidak ada lagi yang bergantung padanya. Baik Anda mengganti nama kolom, memisahkan tabel, atau memperketat constraint, polanya tetap sama. Harganya adalah kesabaran dan koordinasi yang cermat. Imbalannya adalah zero downtime dan zero query yang rusak.