Pelanggaran First Normal Form terlihat dari isi satu sel. Pelanggaran Second Normal Form (2NF) lebih halus: isinya atomik, barisnya pun unik, tetapi ada kolom yang menempel di tabel yang salah hanya karena tabel itu kebetulan punya kunci komposit. 2NF berlaku spesifik untuk tabel ber-primary key komposit — tabel dengan primary key satu kolom otomatis lolos — dan musuhnya bernama ketergantungan parsial: kolom non-kunci yang bergantung hanya pada sebagian kunci. Chapter ini membangun tabel enrollment versi kotor yang menyimpan nama mahasiswa dobel, memancing anomali updatenya secara nyata, lalu memperbaikinya menjadi tiga tabel yang bersih di database latihan belajar_rdb.
Kapan 2NF Relevan #
Aturan 2NF berbunyi: setiap kolom non-kunci harus bergantung pada kunci utuh, bukan sebagianya. Kata “utuh” hanya bermakna bila kuncinya terdiri dari lebih satu kolom — itulah sebabnya pembahasan 2NF muncul tepat setelah kita mengenal junction table, karena tabel perantara adalah tabel yang primary key-nya hampir selalu komposit. Sebaliknya, tabel dengan primary key satu kolom seperti pelanggan(id) tidak mungkin melanggar 2NF: tidak ada “sebagian kunci” yang bisa dimanfaatkan kolom lain. Jadi pertanyaan pertama saat mengaudit 2NF selalu sama — apakah tabel ini punya composite primary key? Bila ya, periksa setiap kolom non-kunci; bila tidak, lanjut ke 3NF.
Ketergantungan Parsial pada enrollment #
Kasus klasiknya persis seperti junction table kita: tabel nilai dengan kunci (mahasiswa_id, kursus_id). Versi kotor menambahkan dua kolom kenyamanan — nama_mahasiswa dan nama_kursus — supaya laporan bisa dibaca tanpa JOIN:
DROP DATABASE IF EXISTS belajar_rdb;
CREATE DATABASE belajar_rdb;
USE belajar_rdb;
CREATE TABLE nilai_kotor (
mahasiswa_id INT NOT NULL,
kursus_id INT NOT NULL,
nama_mahasiswa VARCHAR(50) NOT NULL,
nama_kursus VARCHAR(50) NOT NULL,
nilai DECIMAL(5,2) NOT NULL,
PRIMARY KEY (mahasiswa_id, kursus_id)
);
INSERT INTO nilai_kotor (mahasiswa_id, kursus_id, nama_mahasiswa, nama_kursus, nilai) VALUES
(1, 1, 'Budi Santoso', 'Basis Data', 87.50),
(1, 3, 'Budi Santoso', 'Jaringan Komputer', 78.00),
(2, 1, 'Siti Rahayu', 'Basis Data', 92.00),
(2, 2, 'Siti Rahayu', 'Algoritma dan Pemrograman', 84.50),
(3, 2, 'Andi Wijaya', 'Algoritma dan Pemrograman', 74.00),
(4, 1, 'Rina Melati', 'Basis Data', 88.25),
(4, 3, 'Rina Melati', 'Jaringan Komputer', 90.00);Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 1 row affected (0.00 sec)
Database changed
Query OK, 0 rows affected (0.00 sec)
Query OK, 7 rows affected (0.00 sec)
Records: 7 Duplicates: 0 Warnings: 0SELECT * FROM nilai_kotor ORDER BY mahasiswa_id, kursus_id;Output yang Dihasilkan:
+--------------+-----------+----------------+---------------------------+-------+
| mahasiswa_id | kursus_id | nama_mahasiswa | nama_kursus | nilai |
+--------------+-----------+----------------+---------------------------+-------+
| 1 | 1 | Budi Santoso | Basis Data | 87.50 |
| 1 | 3 | Budi Santoso | Jaringan Komputer | 78.00 |
| 2 | 1 | Siti Rahayu | Basis Data | 92.00 |
| 2 | 2 | Siti Rahayu | Algoritma dan Pemrograman | 84.50 |
| 3 | 2 | Andi Wijaya | Algoritma dan Pemrograman | 74.00 |
| 4 | 1 | Rina Melati | Basis Data | 88.25 |
| 4 | 3 | Rina Melati | Jaringan Komputer | 90.00 |
+--------------+-----------+----------------+---------------------------+-------+
7 rows in set (0.00 sec)Tabel ini lolos 1NF: setiap sel atomik, setiap pasangan unik. Kerusakannya ada di pola ketergantungan. Kolom nilai benar — nilainya ditentukan oleh pasangan lengkap: nilai Budi di Basis Data berbeda dengan nilai Budi di Jaringan, jadi nilai memang butuh kedua bagian kunci. Kolom nama_mahasiswa berbeda cerita: namanya sudah ditentukan penuh oleh mahasiswa_id saja, kolom kursus_id tidak menambah apa pun; sama halnya nama_kursus bergantung hanya pada kursus_id. Diagram ketergantungan berikut memperlihatkan dua panah yang hanya menyentuh separuh kunci — pelanggaran 2NF:
Konsekuensi ketergantungan parsial terukur pada duplikasi: nama Budi tersimpan dua kali, nama Siti dua kali, nama kursus Basis Data tiga kali. Semua salinan itu adalah fakta yang sama — dan fakta yang tersimpan berulang selalu membawa anomali pembaruan.
Anomali Update Akibat Duplikasi #
Siti resmi berganti nama menjadi Siti Rahayu Kusuma. Pada tabel yang bersih, perubahan ini menyentuh satu baris; pada nilai_kotor ia menyentuh semua baris milik Siti:
UPDATE nilai_kotor SET nama_mahasiswa = 'Siti Rahayu Kusuma' WHERE mahasiswa_id = 2;Output yang Dihasilkan:
Query OK, 2 rows affected (0.00 sec)
Rows matched: 2 Changed: 2 Warnings: 0SELECT * FROM nilai_kotor WHERE mahasiswa_id = 2 ORDER BY kursus_id;Output yang Dihasilkan:
+--------------+-----------+--------------------+---------------------------+-------+
| mahasiswa_id | kursus_id | nama_mahasiswa | nama_kursus | nilai |
+--------------+-----------+--------------------+---------------------------+-------+
| 2 | 1 | Siti Rahayu Kusuma | Basis Data | 92.00 |
| 2 | 2 | Siti Rahayu Kusuma | Algoritma dan Pemrograman | 84.50 |
+--------------+-----------+--------------------+---------------------------+-------+
2 rows in set (0.00 sec)Output UPDATE melaporkan dua baris berubah — tepat jumlah salinan nama Siti di tabel ini. Dengan tiga puluh kursus yang diambil, satu penggantian nama menjadi tiga puluh pembaruan, dan satu proses yang gagal di tengah jalan meninggalkan satu nama lama yang tersangkut. Masalah kerabatnya mengikuti pola yang sudah kita kenal dari chapter normalisasi: mahasiswa baru tanpa nilai tidak bisa tercatat (anomali insert), dan menghapus nilai terakhir seorang mahasiswa ikut menghapus identitasnya (anomali delete). Akar semua gejala ini satu: fakta identitas mahasiswa dan fakta identitas kursus menempel pada tabel yang rumahnya seharusnya hanya untuk fakta relasi.
Perbaikan: Fakta Kembali ke Rumahnya Masing-Masing #
Perbaikan 2NF memindahkan setiap fakta ke tabel yang kuncinya sepenuhnya menentukannya. Nama mahasiswa pulang ke tabel mahasiswa dengan kunci id; nama kursus pulang ke tabel kursus; tabel relasi tinggal menyimpan pasangan kunci dan nilai — persis desain junction table yang kita bangun sebelumnya, kini dengan alasan teori yang lengkap:
CREATE TABLE mahasiswa (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(50) NOT NULL
);
CREATE TABLE kursus (
id INT AUTO_INCREMENT PRIMARY KEY,
kode VARCHAR(10) NOT NULL UNIQUE,
nama VARCHAR(50) NOT NULL
);
CREATE TABLE enrollment (
mahasiswa_id INT NOT NULL,
kursus_id INT NOT NULL,
nilai DECIMAL(5,2) NOT NULL,
PRIMARY KEY (mahasiswa_id, kursus_id),
CONSTRAINT fk_enr_mahasiswa
FOREIGN KEY (mahasiswa_id) REFERENCES mahasiswa(id),
CONSTRAINT fk_enr_kursus
FOREIGN KEY (kursus_id) REFERENCES kursus(id)
);
INSERT INTO mahasiswa (id, nama) VALUES
(1, 'Budi Santoso'),
(2, 'Siti Rahayu'),
(3, 'Andi Wijaya'),
(4, 'Rina Melati');
INSERT INTO kursus (id, kode, nama) VALUES
(1, 'BD001', 'Basis Data'),
(2, 'AL001', 'Algoritma dan Pemrograman'),
(3, 'JK001', 'Jaringan Komputer');
INSERT INTO enrollment (mahasiswa_id, kursus_id, nilai) VALUES
(1, 1, 87.50),
(1, 3, 78.00),
(2, 1, 92.00),
(2, 2, 84.50),
(3, 2, 74.00),
(4, 1, 88.25),
(4, 3, 90.00);Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
Query OK, 4 rows affected (0.00 sec)
Records: 4 Duplicates: 0 Warnings: 0
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0
Query OK, 7 rows affected (0.00 sec)
Records: 7 Duplicates: 0 Warnings: 0Struktur hasil perbaikan: enrollment kini hanya memuat kolom yang bergantung pada kunci komposit utuh — pasangan dan nilai. Kolom nama tidak lagi menumpang di sisi mana pun. Sekarang kita ulangi skenario penggantian nama Siti:
UPDATE mahasiswa SET nama = 'Siti Rahayu Kusuma' WHERE id = 2;Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0Satu baris berubah — bandingkan dengan dua baris pada versi kotor. Laporan lengkap tetap tersusun kapan pun dibutuhkan lewat JOIN tiga tabel, dan nama baru langsung terpakai di semua barisnya tanpa satu pun salinan yang harus diubah manual:
SELECT m.nama AS mahasiswa, k.nama AS kursus, e.nilai
FROM enrollment e
JOIN mahasiswa m ON e.mahasiswa_id = m.id
JOIN kursus k ON e.kursus_id = k.id
ORDER BY e.mahasiswa_id, e.kursus_id;Output yang Dihasilkan:
+--------------------+---------------------------+-------+
| mahasiswa | kursus | nilai |
+--------------------+---------------------------+-------+
| Budi Santoso | Basis Data | 87.50 |
| Budi Santoso | Jaringan Komputer | 78.00 |
| Siti Rahayu Kusuma | Basis Data | 92.00 |
| Siti Rahayu Kusuma | Algoritma dan Pemrograman | 84.50 |
| Andi Wijaya | Algoritma dan Pemrograman | 74.00 |
| Rina Melati | Basis Data | 88.25 |
| Rina Melati | Jaringan Komputer | 90.00 |
+--------------------+---------------------------+-------+
7 rows in set (0.00 sec)Perbandingan sebelum/sesudahnya tegas: informasi yang sama, tujuh baris laporan yang sama, tetapi versi bersih menyimpan nama mahasiswa tepat satu kali dan nama kursus tepat satu kali. Anomali insert dan delete ikut pulih — mahasiswa baru bisa tercatat tanpa nilai, dan menghapus seluruh enrollment seorang mahasiswa tidak menghapus identitasnya.
Membersihkan Database Latihan #
DROP DATABASE belajar_rdb;Output yang Dihasilkan:
Query OK, 4 rows affected (0.00 sec)Referensi #
- CREATE TABLE Statement — MySQL 8.4 Reference Manual
- FOREIGN KEY Constraints — MySQL 8.4 Reference Manual
- JOIN Clause — MySQL 8.4 Reference Manual
Second Normal Form tuntas: ketergantungan parsial dibongkar, kolom kenyamanan dipulangkan ke tabel induknya, dan anomali update dua baris menyusut menjadi satu. Tabel hasil perbaikan kini bersih dari dua masalah — namun masih ada satu bentuk ketergantungan yang lolos dari 2NF: kolom non-kunci yang bergantung pada kolom non-kunci lain, bukan pada kunci sama sekali. Kasusnya akrab di data alamat: kota yang selalu mengikuti kode pos. Chapter berikutnya menutup tangga bentuk normal dengan Third Normal Form.