September 27, 2026
Migrasi Database.
Kenapa perubahan skema perlu dikelola lewat migrasi, konsep up/down dan versioning, pola zero-downtime expand-contract, contoh Laravel dan SQL murni, serta backup dengan mysqldump.
Migrasi database adalah cara mengelola perubahan skema (tabel, kolom, index) sebagai file kode yang berurutan dan ter-versi, bukan sebagai perintah yang diketik manual di phpMyAdmin . Ibaratnya Git untuk struktur database.
1. Kenapa Perlu Migrasi?
Tanpa migrasi, cerita klasiknya begini: developer A menambah kolom di laptopnya, lupa memberitahu, lalu aplikasi di staging error “Unknown column”. Migrasi menyelesaikan ini:
| Masalah tanpa migrasi | Dengan migrasi |
|---|---|
| Skema tiap environment berbeda-beda | Semua environment menjalankan file yang sama |
| Tidak tahu perubahan apa yang sudah diterapkan | Ada tabel pencatat versi |
| Perubahan skema tidak ikut code review | Migrasi masuk commit & pull request |
| Setup project baru butuh dump manual | Cukup migrate dari nol |
| Deploy butuh langkah manual | Migrasi dijalankan otomatis di pipeline |
2. Konsep Dasar
Up dan down
Setiap migrasi punya dua arah:
- up: menerapkan perubahan (misalnya
CREATE TABLE). - down: membatalkannya (misalnya
DROP TABLE).
Versioning
Setiap file diberi nomor urut atau timestamp, misalnya 2026_09_27_134000_create_orders_table. Tool migrasi menyimpan daftar migrasi yang sudah dijalankan di tabel khusus (Laravel: migrations, Flyway: flyway_schema_history). Saat migrate dijalankan, hanya file yang belum tercatat yang dieksekusi, sesuai urutan.
Aturan emas
Penting: Migrasi yang sudah dijalankan di environment bersama (staging/produksi) tidak boleh diedit. Jika ada kesalahan, buat migrasi baru yang memperbaikinya. Mengedit file lama tidak akan dijalankan ulang di server yang sudah mencatatnya, sehingga skema antar-environment menjadi berbeda.
3. Contoh dengan Laravel
Membuat file migrasi:
php artisan make:migration create_orders_table
php artisan make:migration add_notes_to_orders_table --table=orders
Isi migrasi (Laravel 9+ memakai anonymous class):
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('customer_id')->constrained()->restrictOnDelete();
$table->decimal('total', 12, 2)->default(0);
$table->enum('status', ['pending', 'paid', 'cancelled'])->default('pending');
$table->timestamps();
$table->index(['customer_id', 'status']);
});
}
public function down(): void
{
Schema::dropIfExists('orders');
}
};
Menambah kolom:
public function up(): void
{
Schema::table('orders', function (Blueprint $table) {
$table->text('notes')->nullable()->after('status');
});
}
public function down(): void
{
Schema::table('orders', function (Blueprint $table) {
$table->dropColumn('notes');
});
}
Perintah yang sering dipakai:
| Perintah | Fungsi |
|---|---|
php artisan migrate | Jalankan migrasi yang belum dijalankan |
php artisan migrate:status | Lihat status tiap migrasi |
php artisan migrate --pretend | Tampilkan SQL tanpa menjalankannya |
php artisan migrate:rollback | Batalkan batch terakhir |
php artisan migrate:rollback --step=1 | Batalkan satu migrasi terakhir |
php artisan migrate:fresh --seed | Drop semua tabel, migrasi ulang, lalu seed |
php artisan migrate --force | Wajib di produksi (melewati konfirmasi) |
Peringatan:
migrate:fresh,migrate:reset, danmigrate:refreshmenghapus data. Hanya untuk lokal/testing, jangan pernah di produksi.
4. Migrasi dengan SQL Murni
Tanpa framework, pola yang sama bisa dibuat dengan file SQL bernomor dan tabel pencatat:
migrations/
├── 001_create_customers.up.sql
├── 001_create_customers.down.sql
├── 002_create_orders.up.sql
├── 002_create_orders.down.sql
└── 003_add_notes_to_orders.up.sql
-- 003_add_notes_to_orders.up.sql
ALTER TABLE orders ADD COLUMN notes TEXT NULL AFTER status;
-- 003_add_notes_to_orders.down.sql
ALTER TABLE orders DROP COLUMN notes;
Tabel pencatat versi:
CREATE TABLE IF NOT EXISTS schema_migrations (
version VARCHAR(50) PRIMARY KEY,
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
Runner sederhana dengan Bash:
#!/usr/bin/env bash
set -euo pipefail
DB="toko"
for f in migrations/*.up.sql; do
v=$(basename "$f" .up.sql)
applied=$(mysql -N -e "SELECT COUNT(*) FROM schema_migrations WHERE version='$v'" "$DB")
if [ "$applied" = "0" ]; then
echo "→ applying $v"
mysql "$DB" < "$f"
mysql -e "INSERT INTO schema_migrations (version) VALUES ('$v')" "$DB"
fi
done
Kredensial sebaiknya disimpan di ~/.my.cnf (bagian [client], permission 600) supaya password tidak muncul di command line. Untuk kebutuhan serius, gunakan tool siap pakai seperti Flyway, Liquibase, golang-migrate, atau dbmate.
Catatan: Karena DDL di MySQL memicu implicit commit, migrasi yang berisi beberapa
ALTERtidak atomik. Jika statement ketiga gagal, dua statement pertama sudah permanen. Buat satu migrasi untuk satu perubahan logis, dan pastikan migrasi aman dijalankan ulang (IF NOT EXISTS) bila memungkinkan.
5. Zero-Downtime: Pola Expand-Contract
Saat deploy, ada jeda ketika kode lama dan kode baru berjalan bersamaan (rolling deploy, beberapa server, worker yang belum restart). Perubahan skema yang merusak kode lama, seperti rename atau drop kolom, akan menimbulkan error di jeda tersebut.
Solusinya adalah expand-contract (parallel change): pecah perubahan yang merusak menjadi beberapa deploy yang masing-masing aman.
Contoh: mengganti nama kolom name menjadi full_name.
| Tahap | Skema | Kode aplikasi |
|---|---|---|
| 1. Expand | Tambah kolom full_name (nullable) | Masih membaca & menulis name |
| 2. Dual write | — | Menulis ke kedua kolom, masih membaca name |
| 3. Backfill | Salin data lama name → full_name secara bertahap | — |
| 4. Switch read | — | Membaca full_name, tetap dual write |
| 5. Stop old write | — | Hanya menulis full_name |
| 6. Contract | Drop kolom name | — |
Backfill bertahap agar tidak mengunci tabel lama-lama:
UPDATE users SET full_name = name
WHERE full_name IS NULL AND id BETWEEN 1 AND 10000;
-- lanjutkan per 10.000 id
Setiap tahap bisa di-rollback dengan aman karena kode lama masih kompatibel dengan skema yang ada.
Aturan praktis perubahan skema
| Perubahan | Aman langsung? | Catatan |
|---|---|---|
| Tambah tabel baru | Ya | |
| Tambah kolom nullable / ber-default | Ya | MySQL 8.0.12+ mendukung ALGORITHM=INSTANT untuk ADD COLUMN |
| Tambah index | Umumnya ya | Online (INPLACE, DML tetap jalan), tapi berat di tabel besar |
Tambah kolom NOT NULL tanpa default | Tidak | Insert dari kode lama akan gagal |
| Rename kolom/tabel | Tidak | Pakai expand-contract |
| Drop kolom | Tidak langsung | Hapus pemakaian di kode dulu, deploy, baru drop |
| Ubah tipe kolom | Tidak | Biasanya rebuild tabel (copy), lock lama; pakai kolom baru + backfill |
Untuk tabel sangat besar, tool seperti gh-ost atau pt-online-schema-change melakukan perubahan lewat tabel bayangan tanpa lock panjang.
6. Backup Sebelum Migrasi: mysqldump
Selalu backup sebelum migrasi produksi, sekecil apa pun perubahannya.
# backup satu database (InnoDB) tanpa mengunci tabel
mysqldump --single-transaction --routines --triggers --events \
-u root -p toko > toko_$(date +%F_%H%M).sql
# versi terkompresi
mysqldump --single-transaction --routines --events -u root -p toko \
| gzip > toko_$(date +%F).sql.gz
# hanya struktur, tanpa data
mysqldump --no-data -u root -p toko > toko_schema.sql
# satu tabel saja
mysqldump --single-transaction -u root -p toko orders > orders.sql
| Opsi | Fungsi |
|---|---|
--single-transaction | Snapshot konsisten untuk InnoDB tanpa lock tabel (tidak konsisten untuk MyISAM) |
--routines | Sertakan stored procedure & function (default tidak ikut) |
--events | Sertakan event scheduler (default tidak ikut) |
--triggers | Sertakan trigger (sudah aktif secara default) |
--no-data | Hanya DDL |
--databases db1 db2 | Beberapa database sekaligus, termasuk CREATE DATABASE |
Restore:
mysql -u root -p toko < toko_2026-09-27_1340.sql
gunzip < toko_2026-09-27.sql.gz | mysql -u root -p toko
Tips: Backup yang tidak pernah dites restore-nya belum bisa disebut backup. Sesekali restore ke database kosong dan cek isinya. Untuk database besar, pertimbangkan MySQL Shell dump utilities (
util.dumpInstance()) yang paralel dan lebih cepat.
7. Checklist Deploy Migrasi
- Migrasi sudah dites di lokal dan di staging dengan data mirip produksi.
down()ditulis dan dites (walaupun di produksi lebih sering roll forward).- Cek SQL yang akan dijalankan (
migrate --pretend). - Backup database.
- Perkirakan durasi untuk tabel besar; jadwalkan saat trafik rendah.
- Perubahan yang merusak dipecah dengan expand-contract.
- Jalankan migrasi sebelum kode baru aktif (untuk tahap expand) dan setelah kode lama hilang (untuk tahap contract).
8. Kesalahan Umum
| Masalah | Solusi |
|---|---|
| Mengedit migrasi lama yang sudah jalan di produksi | Buat migrasi baru |
down() kosong atau salah | Tulis kebalikan yang tepat, atau lempar exception jika memang tidak bisa dibalik |
| Migrasi data (update ribuan baris) digabung dengan perubahan skema | Pisahkan; jalankan backfill bertahap |
| Migrasi bergantung pada Model Eloquent terbaru | Pakai query builder/SQL di migrasi, karena model bisa berubah di masa depan |
| Konflik urutan migrasi antar branch | Rebase dan periksa migrate:status sebelum merge |
Error Specified key was too long di MySQL lama | Gunakan MySQL 5.7.7+/8 dengan utf8mb4 + InnoDB DYNAMIC, atau batasi panjang string |
Referensi

Hey! I’m Fanny, the software engineer tending to this digital garden. You can read more about me, or subscribe by email.