Denormalisasi adalah kebalikan normalisasi: kamu sengaja menyimpan salinan data yang sebenarnya sudah ada di tabel lain, supaya query baca lebih cepat atau lebih sederhana. Bayangin kasir Kedai Senja yang menulis nama pelanggan langsung di setiap nota, supaya gak perlu membuka buku pelanggan tiap kali melihat nota. Cepat. Tapi kalau pelanggan ganti nama, nota-nota lama masih menulis nama yang lama, kecuali ada yang rajin membetulkannya satu per satu.
🧪 Tebakan cepat: nama pelanggan disalin ke kolom
orders.customer_name. Siti mengganti namanya jadi "Siti Aminah" di tabelcustomers. Halaman daftar pesanan membacaorders.customer_name. Nama apa yang tampil di pesanan Siti: "Siti" atau "Siti Aminah"? Tebak dulu, jawabannya ada di bagian "Salinan yang basi".
Desain normal: satu JOIN di setiap halaman
Setelah 3NF, nama pelanggan cuma ada di customers, dan orders menyimpan customer_id:
| customers.id | name |
|---|---|
| 1 | Budi |
| 2 | Siti |
| 3 | Andi |
| orders.id | customer_id | total |
|---|---|---|
| 1 | 1 | 56000 |
| 2 | 1 | 22000 |
| 3 | 2 | 33000 |
| 4 | 2 | 18000 |
| 5 | 3 | 15000 |
Halaman daftar pesanan butuh nama pelanggan, jadi query-nya selalu JOIN:
EXPLAIN QUERY PLAN
SELECT o.id, c.name AS customer_name, o.total
FROM orders o
JOIN customers c ON c.id = o.customer_id;
SCAN o
SEARCH c USING INTEGER PRIMARY KEY (rowid=?)
Dua langkah: SQLite membaca semua pesanan, lalu untuk setiap pesanan mencari pelanggannya lewat primary key. Untuk 5 pesanan ini gak terasa. Untuk halaman yang dibuka ribuan kali sehari dengan 5 tabel di-JOIN, itu kerja yang berulang terus.
Denormalisasi: salin kolomnya
ALTER TABLE orders ADD COLUMN customer_name TEXT;
UPDATE orders
SET customer_name = (SELECT name FROM customers WHERE customers.id = orders.customer_id);
EXPLAIN QUERY PLAN
SELECT id, customer_name, total FROM orders;
SCAN orders
ALTER TABLE ... ADD COLUMNmenambah kolom salinan keorders.UPDATE ... SET customer_name = (SELECT ...)mengisi salinannya sekali, daricustomers.- Query halaman sekarang cuma membaca satu tabel. Gak ada
JOIN, gak adaSEARCH c.
Itulah definisinya: menambahkan redundansi data untuk mempercepat query baca. Nama Siti sekarang tersimpan di 3 tempat: 1 baris customers dan 2 baris orders.
Salinan yang basi
UPDATE customers SET name = 'Siti Aminah' WHERE id = 2;
SELECT o.id, c.name AS di_customers, o.customer_name AS di_orders
FROM orders o
JOIN customers c ON c.id = o.customer_id;
| id | di_customers | di_orders |
|---|---|---|
| 1 | Budi | Budi |
| 2 | Budi | Budi |
| 3 | Siti Aminah | Siti |
| 4 | Siti Aminah | Siti |
| 5 | Andi | Andi |
Halaman pesanan masih menulis "Siti". Itu jawaban tebakannya. Database gak tahu orders.customer_name adalah salinan, jadi dia gak ikut mengubahnya. Ini risiko utama denormalisasi: data jadi gak konsisten (anomali saat update) karena redundansi. Desain normal gak punya masalah ini, karena di sana nama Siti cuma ada di satu tempat.
Menjaga salinan tetap segar
Setiap salinan butuh penjaga. Di SQLite (juga MySQL dan PostgreSQL, dengan sintaks yang beda) penjaganya bisa trigger: perintah yang otomatis jalan setiap kali sebuah tabel berubah.
CREATE TRIGGER sync_customer_name
AFTER UPDATE OF name ON customers
BEGIN
UPDATE orders SET customer_name = NEW.name WHERE customer_id = NEW.id;
END;
AFTER UPDATE OF name ON customers: jalan setelah kolomnamedicustomersberubah.NEW.namedanNEW.idadalah isi baris pelanggan sesudah diubah.
Sekarang ganti nama Siti menulis 3 baris: 1 di customers dan 2 salinannya di orders. Siti punya 2.000 pesanan? 2.001 baris. Itu harga denormalisasi: baca jadi murah, tulis jadi mahal. Di Laravel, penjaga yang sama sering ditulis sebagai model event atau observer, dan risikonya sama: lupa satu jalur update, salinannya basi.
Teknik denormalisasi lain yang sering dipakai:
- Counter cache:
customers.orders_countmenyimpan jumlah pesanan, supaya gak perluCOUNT(*)setiap kali. Setiap pesanan baru harus menambah angkanya. - Tabel ringkasan: omzet per bulan dihitung sekali dan disimpan. PostgreSQL punya
CREATE MATERIALIZED VIEWuntuk ini; SQLite dan MySQL memakai tabel biasa yang diisi ulang secara berkala.
Kapan denormalisasi tepat
Normalisasi dulu, denormalisasi belakangan, dan cuma kalau ada bukti: query lambat di slow query log atau EXPLAIN. Denormalisasi tepat kalau performa baca jauh lebih penting daripada konsistensi sempurna, misalnya dashboard, laporan, atau halaman yang dibaca jauh lebih sering daripada ditulis. Latihan pelajaran ini memakai aturan praktis berikut:
| Persentase baca | Rata-rata JOIN per query | Saran |
|---|---|---|
| 80% ke atas | 3 ke atas | sangat disarankan |
| 60% ke atas | 2 ke atas | pertimbangkan |
| selain itu | tidak perlu |
Angka ini titik awal diskusi, bukan hukum. Yang menentukan tetap hasil pengukuran.
Dua hal yang bukan alasan denormalisasi:
- JOIN-nya panjang dan susah dibaca tim. Itu masalah kerapian, bukan performa. Bungkus query-nya dengan
VIEW, datanya tetap normal. - Harga di nota yang sudah terbit.
price_paiddiorder_itemsmemang harus disimpan, tapi itu fakta pesanan sendiri (harga hari itu), bukan salinan yang harus ikut berubah. Sejak awal dia bagian dari desain yang normal.
⚠️ Jebakan yang sering ketemu
- Denormalisasi sebelum ada masalah. Tanpa bukti query lambat, kamu cuma menambah salinan yang harus dijaga.
- Lupa satu jalur update. Trigger menjaga perubahan lewat SQL, tapi import data atau script lain yang mematikan trigger bisa meninggalkan salinan basi.
- Mengira salinan otomatis ikut. Database gak tahu dua kolom berisi data yang sama. Tanpa trigger atau kode penjaga, salinan gak pernah berubah.
- Batas di latihan. Pakai
>=: 80% dan 3 JOIN tepat di batas masih "sangat disarankan", 60% dengan 1 JOIN "tidak perlu".
TL;DR: Denormalisasi = sengaja menambahkan redundansi data (salinan kolom, counter cache, tabel ringkasan) untuk mempercepat query baca, misalnya orders.customer_name supaya halaman pesanan gak perlu JOIN. Plan-nya turun dari SCAN o + SEARCH c jadi SCAN orders. Risiko utamanya data anomali saat update: Siti ganti nama, salinannya tetap "Siti" sampai dijaga trigger atau kode, dan setiap ganti nama jadi menulis banyak baris. Lakukan setelah normalisasi, kalau ada bukti, dan kalau performa baca lebih penting daripada konsistensi sempurna.