Skema hasil pemodelan ER bisa saja berbentuk satu tabel raksasa yang menyimpan segalanya — dan database tidak akan menolaknya. Masalahnya muncul belakangan: data pelanggan yang sama ditulis berulang di banyak baris, perubahan satu fakta menuntut perubahan banyak baris, dan laporan mulai saling bertentangan. Normalisasi adalah prosedur bertahap untuk memecah tabel seperti itu berdasarkan jenis ketergantungan antar kolomnya. Chapter ini menormalkan tabel pesanan toko online yang sengaja dibuat berantakan, langkah demi langkah, dengan eksekusi nyata di SQLite.
Tabel Denormal dan Anomalinya #
Berikut bentuk awal yang sering muncul dari ekspor spreadsheet: satu baris per pesanan, dengan daftar produk dijepit menjadi satu teks:
import sqlite3
koneksi = sqlite3.connect(":memory:")
koneksi.executescript("""
CREATE TABLE pesanan_denormal (
pesanan_id INTEGER,
nama_pelanggan TEXT,
kota TEXT,
tanggal TEXT,
daftar_produk TEXT,
total INTEGER
);
INSERT INTO pesanan_denormal VALUES
(1, 'Budi Santoso', 'Jakarta', '2024-06-01',
'Smartphone X1 x1; Kemeja Batik x2', 4000000),
(2, 'Siti Rahayu', 'Bandung', '2024-06-03',
'TWS Earbuds x1', 750000),
(3, 'Budi Santoso', 'Jakarta', '2024-07-20',
'Kemeja Batik x1', 250000);
""")
for b in koneksi.execute("SELECT * FROM pesanan_denormal"):
print(b)Output yang Dihasilkan:
(1, 'Budi Santoso', 'Jakarta', '2024-06-01', 'Smartphone X1 x1; Kemeja Batik x2', 4000000)
(2, 'Siti Rahayu', 'Bandung', '2024-06-03', 'TWS Earbuds x1', 750000)
(3, 'Budi Santoso', 'Jakarta', '2024-07-20', 'Kemeja Batik x1', 250000)Tabel di atas menyimpan tiga jenis fakta sekaligus: siapa pelanggannya, kapan pesanannya, dan produk apa saja yang dibeli. Akibatnya data Budi tertulis dua kali. Anomali update muncul ketika Budi pindah kota dan perubahan hanya menyentuh satu baris:
koneksi.execute("UPDATE pesanan_denormal SET kota = 'Depok' WHERE pesanan_id = 3")
for b in koneksi.execute("""
SELECT nama_pelanggan, COUNT(DISTINCT kota) AS jumlah_kota
FROM pesanan_denormal GROUP BY nama_pelanggan
HAVING COUNT(DISTINCT kota) > 1
"""):
print(b)Output yang Dihasilkan:
('Budi Santoso', 2)Satu pelanggan, dua kota — laporan mana yang benar? Normalisasi mencegah pertanyaan seperti itu dengan mencegah duplikasi fakta.
1NF: Satu Nilai Atomik per Sel #
Bentuk normal pertama (1NF) mensyaratkan setiap sel berisi satu nilai atomik dan setiap baris unik. Kolom daftar_produk jelas melanggar aturan itu, jadi kita pecah menjadi satu baris per produk dengan primary key komposit (pesanan_id, produk_id):
koneksi.executescript("""
CREATE TABLE pesanan_1nf (
pesanan_id INTEGER NOT NULL,
produk_id INTEGER NOT NULL,
tanggal TEXT NOT NULL,
nama_pelanggan TEXT NOT NULL,
kota TEXT,
nama_produk TEXT NOT NULL,
harga INTEGER NOT NULL,
jumlah INTEGER NOT NULL,
PRIMARY KEY (pesanan_id, produk_id)
);
INSERT INTO pesanan_1nf VALUES
(1, 1, '2024-06-01', 'Budi Santoso', 'Jakarta', 'Smartphone X1', 3500000, 1),
(1, 4, '2024-06-01', 'Budi Santoso', 'Jakarta', 'Kemeja Batik', 250000, 2),
(2, 3, '2024-06-03', 'Siti Rahayu', 'Bandung', 'TWS Earbuds', 750000, 1),
(3, 4, '2024-07-20', 'Budi Santoso', 'Jakarta', 'Kemeja Batik', 250000, 1);
""")
for b in koneksi.execute("SELECT * FROM pesanan_1nf"):
print(b)Output yang Dihasilkan:
(1, 1, '2024-06-01', 'Budi Santoso', 'Jakarta', 'Smartphone X1', 3500000, 1)
(1, 4, '2024-06-01', 'Budi Santoso', 'Jakarta', 'Kemeja Batik', 250000, 2)
(2, 3, '2024-06-03', 'Siti Rahayu', 'Bandung', 'TWS Earbuds', 750000, 1)
(3, 4, '2024-07-20', 'Budi Santoso', 'Jakarta', 'Kemeja Batik', 250000, 1)2NF: Hilangkan Ketergantungan Parsial #
Bentuk normal kedua (2NF) berlaku untuk tabel ber-primary key komposit: kolom non-kunci harus bergantung pada seluruh kunci, bukan sebagian. Di pesanan_1nf, kolom nama_produk dan harga hanya bergantung pada produk_id — sebagian kunci. Ketergantungan parsial inilah yang membuat harga Kemeja Batik harus tertulis konsisten di banyak baris. Solusinya: angkat fakta produk ke tabel sendiri:
CREATE TABLE produk AS
SELECT DISTINCT produk_id, nama_produk, harga FROM pesanan_1nf;
CREATE TABLE pesanan_2nf AS
SELECT pesanan_id, produk_id, tanggal, nama_pelanggan, kota, jumlah
FROM pesanan_1nf;
SELECT * FROM produk ORDER BY produk_id;Output yang Dihasilkan:
(1, 'Smartphone X1', 3500000)
(3, 'TWS Earbuds', 750000)
(4, 'Kemeja Batik', 250000)Harga kini hanya hidup di satu baris produk — anomali update harga berhenti ada.
3NF: Hilangkan Ketergantungan Transitif #
Bentuk normal ketiga (3NF) melarang kolom non-kunci bergantung pada kolom non-kunci lain. Pada pesanan_2nf, kolom kota bergantung pada nama_pelanggan, bukan langsung pada kunci pesanan — ketergantungan transitif pesanan_id → nama_pelanggan → kota. Fakta pelanggan diangkat ke tabelnya sendiri, dan struktur akhirnya persis skema toko online dari chapter Pemodelan Data:
CREATE TABLE pelanggan AS
SELECT DISTINCT nama_pelanggan AS nama, kota FROM pesanan_2nf;
CREATE TABLE pesanan AS
SELECT DISTINCT pesanan_id, nama_pelanggan, tanggal FROM pesanan_2nf;
CREATE TABLE detail_pesanan AS
SELECT pesanan_id, produk_id, jumlah FROM pesanan_2nf;
SELECT * FROM pelanggan;Output yang Dihasilkan:
('Budi Santoso', 'Jakarta')
('Siti Rahayu', 'Bandung')Laporan yang tadinya tinggal dibaca dari satu tabel kini disusun ulang lewat join — dan hasilnya harus identik dengan data awal:
SELECT ps.pesanan_id, pl.nama, SUM(p.harga * d.jumlah) AS total
FROM pesanan ps
JOIN pelanggan pl ON pl.nama = ps.nama_pelanggan
JOIN detail_pesanan d ON d.pesanan_id = ps.pesanan_id
JOIN produk p ON p.produk_id = d.produk_id
GROUP BY ps.pesanan_id ORDER BY ps.pesanan_idOutput yang Dihasilkan:
(1, 'Budi Santoso', 4000000)
(2, 'Siti Rahayu', 750000)
(3, 'Budi Santoso', 250000)Total ketiga pesanan cocok dengan tabel denormal awal — normalisasi memindah struktur, bukan mengubah isi. Mari kita telaah rantai join pada query verifikasi di atas:
JOIN pelanggan pl ON pl.nama = ps.nama_pelanggan: mengambil fakta kota dari satu-satunya tempatnya disimpan — tabel pelanggan.JOIN detail_pesanan d ON d.pesanan_id = ps.pesanan_id: mengambil daftar produk pesanan, pengganti kolom teksdaftar_produkyang dijepit.JOIN produk p ON p.produk_id = d.produk_id: mengambil harga dari tabel produk, tempat harga kini hanya tertulis sekali.SUM(p.harga * d.jumlah): total pesanan tidak lagi disimpan, melainkan dihitung — tidak ada kolomtotalyang bisa kedaluwarsa.
Kapan Berhenti Normalisasi #
Untuk sistem transaksional (OLTP), 3NF adalah titik aman yang memadai: anomali hilang, join masih terkendali, dan sebagian besar buku teks berhenti di sini atau lanjut ke BCNF untuk kasus kunci kandidat ganda. Normalisasi lebih jauh jarang memberi keuntungan, sementara setiap pemecahan tabel menambah satu join pada jalur baca. Seluruh langkah pada chapter ini memakai SQL standar; sintaks CREATE TABLE ... AS SELECT yang memindahkan data antar bentuk didokumentasikan di dokumentasi CREATE TABLE SQLite
.
Kita memilih kapan berhenti — sekarang kita lihat sisi sebaliknya secara sengaja: mengapa data warehouse justru membangun tabel gabungan yang meniru bentuk denormal di atas. Materinya ada di chapter Denormalisasi.