Second Normal Form (2NF) adalah aturan normalisasi yang kedua: di tabel yang primary key-nya gabungan dua kolom, setiap kolom lain harus bergantung pada kedua kolom itu, bukan cuma salah satunya. Bayangin buku catatan pesanan di Kedai Senja: tiap baris adalah satu menu di satu nota, misalnya "nota 1, Kopi Susu, 2 gelas". Jumlah gelasnya memang milik baris itu. Tapi nama "Kopi Susu" bukan milik nota, dia milik menunya. Kalau nama menu ditulis ulang di setiap nota, mengganti nama berarti membalik semua halaman, dan pasti ada yang kelewat.
🧪 Tebakan cepat: ada dua baris Kopi Susu di tabel pesanan. Kamu mengganti namanya jadi "Kopi Susu Senja" di baris pesanan 1 saja, lalu menampilkan semua baris produk Kopi Susu. Berapa nama berbeda yang keluar: satu atau dua? Tebak dulu, jawabannya ada di bagian "Ubah satu baris, datanya bertentangan".
Tabel yang kita pakai
Di pelajaran 1NF, isi pesanan Kedai Senja pindah ke order_items, satu baris per produk. Sekarang tiap baris juga mencatat product_id dan jumlahnya (qty):
| order_id | product_id | product_name | qty |
|---|---|---|---|
| 1 | 1 | Kopi Susu | 2 |
| 1 | 4 | Roti Bakar | 1 |
| 2 | 2 | Kopi Susu Aren | 1 |
| 3 | 3 | Teh Tarik | 1 |
| 3 | 1 | Kopi Susu | 1 |
Pesanan 1 dan 2 milik Budi, pesanan 3 milik Siti. Primary key-nya (order_id, product_id). Primary key yang terdiri dari dua kolom atau lebih disebut composite primary key. Yang harus unik adalah pasangannya: order_id 1 boleh muncul dua kali, tapi pasangan (1, 1) cuma boleh ada sekali. Kolom yang bukan bagian primary key (product_name dan qty) disebut kolom non-key.
Bergantung pada apa?
Kolom B bergantung pada kolom A kalau begitu A diketahui, nilai B pasti cuma satu. Istilah resminya functional dependency. Cek dua kolom non-key tadi:
| Kolom | Cukup tahu... | Jenis |
|---|---|---|
product_name |
product_id saja: produk 1 pasti Kopi Susu, di pesanan mana pun |
partial dependency |
qty |
order_id dan product_id: Kopi Susu dipesan 2 di pesanan 1, tapi 1 di pesanan 3 |
full dependency |
Partial dependency artinya kolom non-key cuma bergantung pada sebagian primary key. Syarat 2NF ada dua: tabelnya sudah memenuhi 1NF, dan semua kolom non-key bergantung penuh pada seluruh primary key. Jadi order_items di atas melanggar 2NF karena product_name.
Ubah satu baris, datanya bertentangan
Pemilik kedai mengganti nama Kopi Susu dari halaman pesanan 1. Yang berubah cuma baris itu:
UPDATE order_items SET product_name = 'Kopi Susu Senja'
WHERE order_id = 1 AND product_id = 1;
SELECT order_id, product_id, product_name
FROM order_items
WHERE product_id = 1;
| order_id | product_id | product_name |
|---|---|---|
| 1 | 1 | Kopi Susu Senja |
| 3 | 1 | Kopi Susu |
Produk yang sama sekarang punya dua nama, dan database gak protes. Itu jawaban tebakannya: dua. Masalah ini disebut update anomaly. Ada dua saudaranya:
- Delete anomaly: pesanan 2 dibatalkan (
DELETE FROM order_items WHERE order_id = 2;). Kopi Susu Aren cuma ada di pesanan itu, jadi kedai ikut "lupa" bahwa Kopi Susu Aren ada di menu. - Insert anomaly: menu baru, Pisang Goreng, belum pernah dipesan. Dia gak punya tempat, karena setiap baris butuh
order_id.
Pecah tabelnya
Kolom yang cuma bergantung pada product_id pindah ke tabel yang primary key-nya product_id itu sendiri:
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE order_items (
order_id INTEGER NOT NULL,
product_id INTEGER NOT NULL REFERENCES products(id),
qty INTEGER NOT NULL,
PRIMARY KEY (order_id, product_id) -- composite primary key
);
INSERT INTO products VALUES (1, 'Kopi Susu'), (2, 'Kopi Susu Aren'), (3, 'Teh Tarik'), (4, 'Roti Bakar');
INSERT INTO order_items VALUES (1, 1, 2), (1, 4, 1), (2, 2, 1), (3, 3, 1), (3, 1, 1);
productsmenyimpan nama setiap produk sekali. Pisang Goreng tinggal ditambah ke sini, walau belum ada yang memesan.order_itemstinggal menyimpan yang memang milik pasangan pesanan dan produk:qty.REFERENCES products(id)menghubungkanproduct_idkeproducts. Detailnya di pelajaran Primary Key & Foreign Key.NOT NULLdi kedua kolom key: SQLite membolehkanNULLdi composite primary key kalau gak ditulis, MySQL dan PostgreSQL gak.
Sekarang ganti nama cukup satu baris, dan semua pesanan membacanya lewat JOIN:
UPDATE products SET name = 'Kopi Susu Senja' WHERE id = 1;
SELECT oi.order_id, p.name AS product_name, oi.qty
FROM order_items oi
JOIN products p ON p.id = oi.product_id
WHERE oi.product_id = 1;
| order_id | product_name | qty |
|---|---|---|
| 1 | Kopi Susu Senja | 2 |
| 3 | Kopi Susu Senja | 1 |
Gak mungkin bertentangan lagi, karena namanya cuma ada di satu tempat. Pesanan 2 dibatalkan pun, Kopi Susu Aren tetap ada di products.
Kapan 2NF perlu dicek
Partial dependency cuma mungkin terjadi kalau primary key-nya gabungan. Kalau primary key cuma satu kolom (misalnya id auto-increment), gak ada "sebagian" dari key itu, jadi tabel yang sudah 1NF otomatis memenuhi 2NF. Cara ceknya bisa dihitung:
| Kolom di primary key | Kolom key yang jadi sandaran | Hasil |
|---|---|---|
| 2 | 1 (product_name ke product_id) |
partial dependency |
| 2 | 2 (qty ke keduanya) |
full dependency |
| 1 | 1 (name ke products.id) |
tidak mungkin partial |
| 3 | 2 | partial dependency |
⚠️ Jebakan yang sering ketemu
- Mencari partial dependency di tabel ber-
idtunggal. Gak mungkin ada. Masalah di tabel seperti itu, kalau ada, urusan 3NF di pelajaran berikutnya. - Ikut memindahkan kolom yang bergantung penuh. Harga yang dibayar di pesanan itu (
price_paid) bergantung pada pesanan dan produk, karena harga Kopi Susu bisa naik bulan depan. Dia tetap diorder_items. - Lupa kolom yang bersandar ke kolom key satunya.
order_datecuma bergantung padaorder_id, jadi dia juga partial dan tempatnya di tabelorders. - Urutan cek di latihan. Cek dulu primary key satu kolom (
pkColumns <= 1), baru bandingkandependentOndenganpkColumns. Kalau terbalik,(1, 1)keluar sebagai "full dependency".
TL;DR: 2NF = sudah 1NF, dan setiap kolom non-key bergantung pada seluruh primary key. Pelanggarannya, partial dependency, cuma bisa muncul di composite primary key: product_name di order_items (order_id, product_id) cuma bergantung pada product_id. Akibatnya update anomaly (satu produk punya dua nama), delete anomaly dan insert anomaly. Solusinya: pindahkan kolom itu ke tabel sendiri (products) dan sambungkan dengan product_id. Primary key satu kolom = tidak mungkin partial.