Menyimpan seluruh data toko dalam satu tabel besar terasa praktis di awal, tetapi setiap fakta yang ditulis berulang kali adalah bom waktu: satu baris terlewat saat mengubah data, dan database langsung saling bertentangan. Normalisasi adalah prosedur sistematis untuk memecah tabel agar setiap fakta tersimpan tepat satu kali — dan hasilnya siap membangun relasi antar tabel di chapter-chapter selanjutnya.
Tabel Tunggal untuk Semua Data #
Mari mulai dari desain terburuk: satu tabel menyimpan pesanan beserta daftar produknya sebagai satu teks panjang.
CREATE TABLE penjualan_mentah (
no_pesanan INTEGER PRIMARY KEY,
nama_pelanggan TEXT NOT NULL,
kota TEXT,
daftar_produk TEXT NOT NULL
);
INSERT INTO penjualan_mentah VALUES
(1, 'Budi', 'Jakarta', 'Kopi Gayo x2, Gula Aren x1'),
(2, 'Siti', 'Bandung', 'Teh Melati x3'),
(3, 'Ani', 'Jakarta', 'Kopi Gayo x1, Teh Hijau x2');SELECT * FROM penjualan_mentah ORDER BY no_pesanan;Output yang Dihasilkan:
1|Budi|Jakarta|Kopi Gayo x2, Gula Aren x1
2|Siti|Bandung|Teh Melati x3
3|Ani|Jakarta|Kopi Gayo x1, Teh Hijau x2Kolom daftar_produk memuat banyak nilai dalam satu sel, sehingga query “siapa yang membeli Kopi Gayo?” hanya bisa mengandalkan LIKE '%Kopi Gayo%' — total uang tidak bisa dihitung dan harga satu produk tidak bisa diubah tanpa mengurai teksnya. Masalah ini diselesaikan oleh bentuk normal pertama.
1NF: Satu Nilai Atomik per Sel #
First Normal Form (1NF) mensyaratkan setiap sel berisi satu nilai atomik dan melarang kelompok kolom berulang seperti produk1, produk2, produk3. Solusinya: satu baris per item pesanan.
CREATE TABLE catatan_penjualan (
no_pesanan INTEGER NOT NULL,
tanggal TEXT NOT NULL,
nama_pelanggan TEXT NOT NULL,
kota TEXT,
produk TEXT NOT NULL,
harga INTEGER NOT NULL,
jumlah INTEGER NOT NULL,
PRIMARY KEY (no_pesanan, produk)
);
INSERT INTO catatan_penjualan VALUES
(1, '2025-01-05', 'Budi', 'Jakarta', 'Kopi Gayo', 45000, 2),
(1, '2025-01-05', 'Budi', 'Jakarta', 'Gula Aren', 25000, 1),
(2, '2025-01-06', 'Siti', 'Bandung', 'Teh Melati', 18000, 3),
(3, '2025-01-07', 'Ani', 'Jakarta', 'Kopi Gayo', 45000, 1),
(3, '2025-01-07', 'Ani', 'Jakarta', 'Teh Hijau', 22000, 2);SELECT * FROM catatan_penjualan ORDER BY no_pesanan, produk;Output yang Dihasilkan:
1|2025-01-05|Budi|Jakarta|Gula Aren|25000|1
1|2025-01-05|Budi|Jakarta|Kopi Gayo|45000|2
2|2025-01-06|Siti|Bandung|Teh Melati|18000|3
3|2025-01-07|Ani|Jakarta|Kopi Gayo|45000|1
3|2025-01-07|Ani|Jakarta|Teh Hijau|22000|2Setiap sel kini atomik, tetapi tanggal, nama_pelanggan, dan kota Budi tertulis dua kali. Duplikasi inilah pemicu tiga anomali klasik berikut.
Anomali pada Tabel 1NF #
Anomali update — Budi pindah ke Depok, tetapi UPDATE hanya menjangkau satu dari dua barisnya:
UPDATE catatan_penjualan SET kota = 'Depok'
WHERE no_pesanan = 1 AND produk = 'Kopi Gayo';SELECT DISTINCT no_pesanan, nama_pelanggan, kota
FROM catatan_penjualan
WHERE no_pesanan = 1;Output yang Dihasilkan:
1|Budi|Jakarta
1|Budi|DepokSatu pelanggan yang sama tercatat bermukim di dua kota; memperbaikinya menuntut UPDATE kedua untuk baris Gula Aren. Dua pekerjaan hanya untuk satu fakta.
Anomali insert — produk baru yang belum pernah dibeli tidak bisa dicatat karena komponen primary key no_pesanan wajib terisi:
INSERT INTO catatan_penjualan (no_pesanan, tanggal, nama_pelanggan, kota, produk, harga, jumlah)
VALUES (NULL, NULL, NULL, NULL, 'Kopi Toraja', 52000, 0);Output yang Dihasilkan:
Runtime error: NOT NULL constraint failed: catatan_penjualan.no_pesanan (19)Anomali delete — pesanan nomor 2 milik Siti dihapus, dan jejak Siti sebagai pelanggan ikut lenyap:
DELETE FROM catatan_penjualan WHERE no_pesanan = 2;SELECT count(*) FROM catatan_penjualan WHERE nama_pelanggan = 'Siti';Output yang Dihasilkan:
02NF: Menghilangkan Ketergantungan Parsial #
Primary key tabel di atas adalah gabungan (no_pesanan, produk). Second Normal Form (2NF) melarang kolom non-key yang hanya bergantung pada sebagian key: tanggal dan nama_pelanggan bergantung pada no_pesanan saja, sedangkan harga bergantung pada produk saja. Solusinya memecah tabel menjadi tiga:
CREATE TABLE pesanan (
no_pesanan INTEGER PRIMARY KEY,
tanggal TEXT NOT NULL,
nama_pelanggan TEXT NOT NULL,
kota TEXT
);
CREATE TABLE produk (
nama TEXT PRIMARY KEY,
harga INTEGER NOT NULL
);
CREATE TABLE item_pesanan (
no_pesanan INTEGER NOT NULL REFERENCES pesanan(no_pesanan),
produk TEXT NOT NULL REFERENCES produk(nama),
jumlah INTEGER NOT NULL,
PRIMARY KEY (no_pesanan, produk)
);Harga Kopi Gayo kini tersimpan satu kali di produk, sehingga anomali insert produk baru hilang; hanya jumlah yang bergantung penuh pada pasangan (no_pesanan, produk).
3NF: Menghilangkan Ketergantungan Transitif #
Third Normal Form (3NF) melarang kolom non-key yang bergantung pada kolom non-key lain. Di pesanan hasil 2NF, kota bergantung pada nama_pelanggan — ketergantungan transitif. Pecah pelanggan menjadi tabel tersendiri, sekaligus ganti kunci utama dengan id INTEGER PRIMARY KEY sesuai praktik industri:
DROP TABLE item_pesanan;
DROP TABLE produk;
DROP TABLE pesanan;
CREATE TABLE pelanggan (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL,
kota TEXT
);
CREATE TABLE pesanan (
id INTEGER PRIMARY KEY,
tanggal TEXT NOT NULL,
pelanggan_id INTEGER NOT NULL REFERENCES pelanggan(id)
);
CREATE TABLE produk (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL,
harga INTEGER NOT NULL
);
CREATE TABLE item_pesanan (
pesanan_id INTEGER NOT NULL REFERENCES pesanan(id),
produk_id INTEGER NOT NULL REFERENCES produk(id),
jumlah INTEGER NOT NULL,
PRIMARY KEY (pesanan_id, produk_id)
);
INSERT INTO pelanggan VALUES
(1, 'Budi', 'Jakarta'),
(2, 'Siti', 'Bandung'),
(3, 'Ani', 'Jakarta');
INSERT INTO pesanan VALUES
(1, '2025-01-05', 1),
(2, '2025-01-06', 2),
(3, '2025-01-07', 3);
INSERT INTO produk VALUES
(1, 'Kopi Gayo', 45000),
(2, 'Gula Aren', 25000),
(3, 'Teh Melati', 18000),
(4, 'Teh Hijau', 22000);
INSERT INTO item_pesanan VALUES
(1, 1, 2), (1, 2, 1), (2, 3, 3), (3, 1, 1), (3, 4, 2);Semua anomali lenyap: kota Budi berubah cukup di satu baris pelanggan, dan menghapus pesanan tidak ikut menghapus pelanggannya. Informasi awal tetap bisa direkonstruksi dengan JOIN:
SELECT pes.tanggal, pel.nama, pel.kota, pr.nama AS produk, pr.harga, i.jumlah
FROM item_pesanan i
JOIN pesanan pes ON i.pesanan_id = pes.id
JOIN pelanggan pel ON pes.pelanggan_id = pel.id
JOIN produk pr ON i.produk_id = pr.id
ORDER BY pes.id, pr.nama;Output yang Dihasilkan:
2025-01-05|Budi|Jakarta|Gula Aren|25000|1
2025-01-05|Budi|Jakarta|Kopi Gayo|45000|2
2025-01-06|Siti|Bandung|Teh Melati|18000|3
2025-01-07|Ani|Jakarta|Kopi Gayo|45000|1
2025-01-07|Ani|Jakarta|Teh Hijau|22000|2Bedah singkat query rekonstruksi di atas:
- Baris 2 (
FROM item_pesanan i): tabel ini jadi titik berangkat karena memuatpesanan_iddanproduk_idyang menautkan semua tabel lain. - Baris 3-5 (
JOIN ... ON ...): tigaJOINmenempelkan datapesanan,pelanggan, danprodukmelalui foreign key masing-masing. - Baris 6 (
ORDER BY pes.id, pr.nama): pengurutan eksplisit agar output selalu konsisten.
| Tahap | Aturan | Duplikasi yang dihapus |
|---|---|---|
| 1NF | Nilai atomik, tanpa kolom berulang | Daftar produk dalam satu sel |
| 2NF | Tanpa ketergantungan parsial pada composite key | tanggal, nama_pelanggan, harga |
| 3NF | Tanpa ketergantungan transitif | kota pada tabel pesanan |
Skema pelanggan-pesanan hasil normalisasi adalah relasi one-to-many: satu pelanggan boleh punya banyak pesanan. Cara merancang dan meng-query relasi tersebut dibahas pada chapter Merancang Relasi One-to-Many.