Konsep tabel perantara sudah kita pegang; sekarang saatnya membangunnya sampai tuntas. Junction table adalah tabel yang seluruh keberadaannya mewakili relasi antara dua entitas: satu barisnya bukan mahasiswa, bukan pula kursus, melainkan satu fakta “mahasiswa ini mengambil kursus ini”. Karena bentuknya khas — kunci komposit, dua foreign key, plus atribut yang dimiliki relasi — junction table layak dibedah sebagai pola tersendiri. Chapter ini membangun tabel enrollment lengkap di database latihan belajar_rdb, membuktikan penolakan pasangan ganda, lalu menggabungkan tiga tabel dalam satu query dan agregasi ringkas.
Anatomi Junction Table #
Sebuah junction table minimal memuat dua kolom penunjuk: mahasiswa_id dan kursus_id. Kunci utamanya dibentuk dari gabungan keduanya — composite primary key (mahasiswa_id, kursus_id) — sehingga setiap pasangan mahasiswa-kursus hanya boleh muncul sekali di seluruh tabel. Dua foreign key mengarah ke masing-masing induk, dan kolom-kolom tambahan seperti nilai serta tanggal_daftar menempel pada tabel ini karena nilainya memang milik pasangan, bukan milik salah satu entitas.
Diagram di atas memperlihatkan posisi ENROLLMENT sebagai anak dari dua induk sekaligus. Kardinalitas “banyak-ke-banyak” asli tetap terbaca: satu mahasiswa terhubung ke banyak baris ENROLLMENT, satu kursus juga terhubung ke banyak baris ENROLLMENT, dan perpotongan keduanya di tabel perantara itulah pasangan yang terjadi.
Praktik: Membangun Tabel enrollment #
Kita bangun ketiga tabel dari nol. Tabel mahasiswa dan kursus berdiri seperti entitas biasa; tabel enrollment menyusul dengan struktur khas junction:
DROP DATABASE IF EXISTS belajar_rdb;
CREATE DATABASE belajar_rdb;
USE belajar_rdb;
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,
sks INT NOT NULL
);
CREATE TABLE enrollment (
mahasiswa_id INT NOT NULL,
kursus_id INT NOT NULL,
nilai DECIMAL(5,2) NULL,
tanggal_daftar DATE 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)
);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, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)Empat keputusan struktur di atas menentukan perilaku junction table:
PRIMARY KEY (mahasiswa_id, kursus_id)— kunci utama berupa gabungan dua kolom; keunikan diperiksa pada pasangan, sehingga mahasiswa1boleh muncul banyak kali (dengan kursus berbeda) dan kursus1juga boleh muncul banyak kali (dengan mahasiswa berbeda).fk_enr_mahasiswadanfk_enr_kursus— dua foreign key bernama eksplisit menegakkan kedua relasi satu-ke-banyak; barisenrollmenttanpa mahasiswa atau tanpa kursus yang sah ditolak server.nilai DECIMAL(5,2) NULL— nilai ujian boleh kosong karena mahasiswa boleh terdaftar sebelum ujian berlangsung;tanggal_daftarwajib terisi karena pendaftaran selalu punya tanggal.- Tanpa kolom
iddanAUTO_INCREMENT— identitas baris junction memang berasal dari pasangan induknya; nomor urut tambahan tidak menambah kekuatan apa pun.
Mengisi Data dan Atribut Milik Relasi #
Kita isi empat mahasiswa, tiga kursus, lalu tujuh baris pendaftaran. Perhatikan bahwa nilai dan tanggal_daftar terisi pada tabel enrollment — bukan pada tabel mahasiswa atau kursus:
INSERT INTO mahasiswa (nama) VALUES
('Budi Santoso'),
('Siti Rahayu'),
('Andi Wijaya'),
('Rina Melati');
INSERT INTO kursus (kode, nama, sks) VALUES
('BD001', 'Basis Data', 3),
('AL001', 'Algoritma dan Pemrograman', 3),
('JK001', 'Jaringan Komputer', 2);
INSERT INTO enrollment (mahasiswa_id, kursus_id, nilai, tanggal_daftar) VALUES
(1, 1, 87.50, '2026-01-12'),
(1, 3, 78.00, '2026-01-12'),
(2, 1, 92.00, '2026-01-13'),
(2, 2, 84.50, '2026-01-13'),
(3, 2, 74.00, '2026-01-14'),
(4, 1, 88.25, '2026-01-15'),
(4, 3, 90.00, '2026-01-15');Output yang Dihasilkan:
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: 0Penempatan nilai di enrollment bukan kebetulan — nilai ujian Budi pada Basis Data (87.50) berbeda dengan nilai Budi pada Jaringan Komputer (78.00), dan berbeda pula dengan nilai Siti pada Basis Data (92.00). Nilai berubah mengikuti kombinasi mahasiswa dan kursus, jadi kolomnya harus hidup di tabel yang satu barisnya merepresentasikan kombinasi tersebut. Bayangkan bila nilai disimpan di tabel mahasiswa: seluruh kursus Budi terpaksa memakai satu nilai yang sama. Sebaliknya bila nilai disimpan di tabel kursus: seluruh mahasiswa Basis Data mendapat nilai seragam. Kedua penempatan itu jelas keliru, dan pola berpikirnya berlaku umum: kolom milik relasi selalu menempel pada tabel perantara.
Menolak Pasangan Ganda #
Composite primary key bekerja sebagai penjaga keunikan pasangan. Kita uji dengan mencoba mendaftarkan Budi (id 1) ke Basis Data (id 1) untuk kedua kalinya — misalnya karena skrip pendaftaran tidak sengaja berjalan dua kali:
-- @error
INSERT INTO enrollment (mahasiswa_id, kursus_id, nilai, tanggal_daftar) VALUES (1, 1, 95.00, '2026-02-01');Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1062 (23000): Duplicate entry '1-1' for key 'enrollment.PRIMARY'
ByeServer menolak penyisipan dengan galat Duplicate entry '1-1': pasangan mahasiswa 1 dan kursus 1 sudah tercatat, dan primary key tidak mengizinkan pasangan yang sama muncul dua kali. Penolakan semacam ini menjaga relasi tetap bersih tanpa perlu SELECT pemeriksaan sebelum INSERT — cukup biarkan constraint yang menolak.
Query Tiga Tabel dan Agregasi #
Data junction baru berarti setelah digabungkan dengan kedua induknya. Query berikut merangkai tiga tabel lewat dua pasangan kunci sekaligus:
SELECT m.nama AS mahasiswa, k.nama AS kursus, e.nilai, e.tanggal_daftar
FROM enrollment e
JOIN mahasiswa m ON e.mahasiswa_id = m.id
JOIN kursus k ON e.kursus_id = k.id
ORDER BY m.id, k.id;Output yang Dihasilkan:
+--------------+---------------------------+-------+----------------+
| mahasiswa | kursus | nilai | tanggal_daftar |
+--------------+---------------------------+-------+----------------+
| Budi Santoso | Basis Data | 87.50 | 2026-01-12 |
| Budi Santoso | Jaringan Komputer | 78.00 | 2026-01-12 |
| Siti Rahayu | Basis Data | 92.00 | 2026-01-13 |
| Siti Rahayu | Algoritma dan Pemrograman | 84.50 | 2026-01-13 |
| Andi Wijaya | Algoritma dan Pemrograman | 74.00 | 2026-01-14 |
| Rina Melati | Basis Data | 88.25 | 2026-01-15 |
| Rina Melati | Jaringan Komputer | 90.00 | 2026-01-15 |
+--------------+---------------------------+-------+----------------+
7 rows in set (0.00 sec)Tujuh baris hasil adalah tujuh pasangan yang terjadi: dua JOIN menyambungkan enrollment ke mahasiswa lewat mahasiswa_id dan ke kursus lewat kursus_id, sehingga nama manusia dan nama mata kuliah bertemu dalam satu baris laporan. Pola join ini persis dua kali pola join satu-ke-banyak dari chapter sebelumnya — junction memang tidak memperkenalkan teknik query baru, hanya mengulang pola yang sama di dua arah.
Agregasi ringkas pun langsung tersedia. Beban studi tiap mahasiswa dihitung dari jumlah pasangan dan jumlah SKS yang menyertainya:
SELECT m.nama,
COUNT(e.kursus_id) AS jumlah_kursus,
SUM(k.sks) AS total_sks
FROM mahasiswa m
LEFT JOIN enrollment e ON m.id = e.mahasiswa_id
LEFT JOIN kursus k ON e.kursus_id = k.id
GROUP BY m.id, m.nama
ORDER BY m.id;Output yang Dihasilkan:
+--------------+---------------+-----------+
| nama | jumlah_kursus | total_sks |
+--------------+---------------+-----------+
| Budi Santoso | 2 | 5 |
| Siti Rahayu | 2 | 6 |
| Andi Wijaya | 1 | 3 |
| Rina Melati | 2 | 5 |
+--------------+---------------+-----------+
4 rows in set (0.00 sec)Sementara rekap kualitas kursus menghitung jumlah peserta dan rata-rata nilai per kursus:
SELECT k.nama AS kursus,
COUNT(e.mahasiswa_id) AS jumlah_mahasiswa,
AVG(e.nilai) AS rata_nilai
FROM kursus k
JOIN enrollment e ON e.kursus_id = k.id
GROUP BY k.id, k.nama
ORDER BY k.id;Output yang Dihasilkan:
+---------------------------+------------------+------------+
| kursus | jumlah_mahasiswa | rata_nilai |
+---------------------------+------------------+------------+
| Basis Data | 3 | 89.250000 |
| Algoritma dan Pemrograman | 2 | 79.250000 |
| Jaringan Komputer | 2 | 84.000000 |
+---------------------------+------------------+------------+
3 rows in set (0.00 sec)Perhatikan perbedaan jenis join pada dua agregat di atas. Query beban studi memakai LEFT JOIN supaya mahasiswa yang belum mengambil kursus apa pun tetap tampil dengan hitungan 0; query rekap kursus memakai JOIN biasa karena kursus tanpa peserta memang tidak punya nilai untuk dirata-rata. Kedua pilihan itu keputusan pelaporan, bukan kebetulan sintaks — dan keduanya hanya mungkin karena relasi banyak-ke-banyak sudah tersimpan dalam bentuk baris yang bisa dihitung.
Membersihkan Database Latihan #
DROP DATABASE belajar_rdb;Output yang Dihasilkan:
Query OK, 3 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
Junction table kini bukan lagi teori: enrollment berdiri dengan composite primary key yang menolak pasangan ganda, dua foreign key yang menjaga rujukan, dan kolom nilai-tanggal_daftar yang menempatkan atribut relasi di rumahnya sendiri. Motif yang sama sesungguhnya muncul dari masalah lain — tabel pelanggan yang menyimpan nama kasir di setiap baris pesanan, atau kolom kota yang mengikuti kode pos. Masalah-masalah tersebut punya nama umum: anomali data akibat struktur yang belum normal. Chapter berikutnya masuk ke teori normalisasi database, kerangka yang mengubah keluhan-keluhan tersebut menjadi daftar perbaikan yang sistematis.