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.

sql
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');
sql
SELECT * FROM penjualan_mentah ORDER BY no_pesanan;

Output yang Dihasilkan:

text
1|Budi|Jakarta|Kopi Gayo x2, Gula Aren x1
2|Siti|Bandung|Teh Melati x3
3|Ani|Jakarta|Kopi Gayo x1, Teh Hijau x2

Kolom 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.

sql
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);
sql
SELECT * FROM catatan_penjualan ORDER BY no_pesanan, produk;

Output yang Dihasilkan:

text
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|2

Setiap 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:

sql
UPDATE catatan_penjualan SET kota = 'Depok'
WHERE no_pesanan = 1 AND produk = 'Kopi Gayo';
sql
SELECT DISTINCT no_pesanan, nama_pelanggan, kota
FROM catatan_penjualan
WHERE no_pesanan = 1;

Output yang Dihasilkan:

text
1|Budi|Jakarta
1|Budi|Depok

Satu 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:

sql
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:

text
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:

sql
DELETE FROM catatan_penjualan WHERE no_pesanan = 2;
sql
SELECT count(*) FROM catatan_penjualan WHERE nama_pelanggan = 'Siti';

Output yang Dihasilkan:

text
0

2NF: 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:

sql
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:

sql
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:

sql
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:

text
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|2

Bedah singkat query rekonstruksi di atas:

  1. Baris 2 (FROM item_pesanan i): tabel ini jadi titik berangkat karena memuat pesanan_id dan produk_id yang menautkan semua tabel lain.
  2. Baris 3-5 (JOIN ... ON ...): tiga JOIN menempelkan data pesanan, pelanggan, dan produk melalui foreign key masing-masing.
  3. Baris 6 (ORDER BY pes.id, pr.nama): pengurutan eksplisit agar output selalu konsisten.
TahapAturanDuplikasi yang dihapus
1NFNilai atomik, tanpa kolom berulangDaftar produk dalam satu sel
2NFTanpa ketergantungan parsial pada composite keytanggal, nama_pelanggan, harga
3NFTanpa ketergantungan transitifkota 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.