Primary key menjaga identitas di dalam satu tabel, tetapi data nyata hidup dalam jaringan hubungan: produk tergabung dalam kategori, ulasan menempel pada produk, dan pembelian mencatat barang yang benar-benar terjual. Tanpa pengawas, tidak ada yang mencegah baris produk menyimpan kategori_id = 9 padahal kategori sembilan tidak pernah ada — data yatim yang merusak laporan. FOREIGN KEY (FK) adalah constraint yang menjaga relasi tersebut: setiap nilai pada kolom anak harus merujuk baris yang benar-benar ada di tabel induk. Pada chapter ini kita akan mendefinisikan FK beserta aksi ON DELETE tiga macam, lalu mengujinya dengan skenario nyata: insert anak tanpa induk, hapus induk ber-CASCADE, dan hapus induk yang diblokir RESTRICT.
Masalah Baris Yatim #
Bayangkan laporan penjualan yang menghitung omzet per kategori. Satu produk menyimpan kategori_id = 9, sementara tabel kategori hanya memuat id 1 sampai 3. Laporan tersebut kehilangan omzet produk itu — bukan karena datinya salah, melainkan karena relasinya putus. Masalah sejenis ini jarang terdeteksi dari aplikasi, karena INSERT tetap berhasil dan data tampak normal. Foreign key memindahkan pemeriksaan relasi ke server: kolom anak diperiksa setiap kali baris baru masuk atau nilai referensi berubah.
Mendefinisikan FOREIGN KEY #
Kita bangun dua tabel: kategori sebagai induk dan produk sebagai anak. Kolom produk.kategori_id akan merujuk ke kategori.id:
CREATE DATABASE belajar_fk;
USE belajar_fk;Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)
Database changedCREATE TABLE kategori (
id INT PRIMARY KEY,
nama VARCHAR(50) NOT NULL
);
CREATE TABLE produk (
id INT PRIMARY KEY,
nama VARCHAR(100) NOT NULL,
harga DECIMAL(10,2) NOT NULL,
kategori_id INT,
CONSTRAINT fk_produk_kategori FOREIGN KEY (kategori_id)
REFERENCES kategori (id)
ON DELETE CASCADE
);Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)Bagian pentingnya ada pada enam baris terakhir. Mari kita bedah satu per satu:
- Baris
kategori_id INT: Kolom anak bertipeINT— wajib sama tipe dengankategori.idyang menjadi rujukannya. Kolom ini sengaja dibiarkan bolehNULLagar produk tanpa kategori tetap sah. - Baris
CONSTRAINT fk_produk_kategori: Nama constraint kita pilih sendiri agar mudah dibaca di pesan galat; tanpa nama, server mengarang nama otomatis sepertiproduk_ibfk_1. - Kata kunci
FOREIGN KEY (kategori_id): Menandaikategori_idsebagai kolom kunci asing pada tabel anak. - Klausa
REFERENCES kategori (id): Menyatakan tabel induk (kategori) beserta kolom yang dirujuk (id). Nilaikategori_idhanya sah bila id tersebut ada di tabel induk — atau bernilaiNULL. - Klausa
ON DELETE CASCADE: Aturan aksi saat baris induk dihapus — di sini baris anak ikut terhapus. Tanpa klausa ini, perilaku bawaannya adalah menolak penghapusan.
Kita isi kedua tabel dan pastikan relasinya tersambung:
INSERT INTO kategori (id, nama) VALUES
(1, 'Alat Tulis'),
(2, 'Buku');
INSERT INTO produk (id, nama, harga, kategori_id) VALUES
(1, 'Buku Tulis 58', 4500.00, 1),
(2, 'Laskar Pelangi', 95000.00, 2);
SELECT id, nama, harga, kategori_id FROM produk ORDER BY id;Output yang Dihasilkan:
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0
+----+----------------+----------+-------------+
| id | nama | harga | kategori_id |
+----+----------------+----------+-------------+
| 1 | Buku Tulis 58 | 4500.00 | 1 |
| 2 | Laskar Pelangi | 95000.00 | 2 |
+----+----------------+----------+-------------+
2 rows in set (0.00 sec)Insert Anak Tanpa Induk Ditolak #
Ujian pertama foreign key adalah baris anak yang mencoba merujuk induk yang tidak ada. Statement berikut memasukkan produk dengan kategori_id = 9:
-- @error
INSERT INTO produk (id, nama, harga, kategori_id) VALUES (3, 'Spidol Whiteboard', 18000.00, 9);Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`belajar_fk`.`produk`, CONSTRAINT `fk_produk_kategori` FOREIGN KEY (`kategori_id`) REFERENCES `kategori` (`id`) ON DELETE CASCADE)
ByeServer menolaknya dengan galat ERROR 1452 (23000) — Cannot add or update a child row: a foreign key constraint fails — diikuti nama constraint fk_produk_kategori yang kita definisikan sendiri. Baris yatim tidak pernah sampai tersimpan, sehingga laporan per kategori di masa depan tetap utuh.
Index Otomatis pada Kolom FK #
Setiap kali foreign key dibuat, MySQL otomatis menambahkan index pada kolom anak bila index serupa belum ada — penghapusan induk harus mencari baris anak dengan cepat, dan tanpa index proses itu memindai seluruh tabel anak. Kita bisa membuktikannya lewat katalog information_schema.STATISTICS:
SELECT INDEX_NAME, COLUMN_NAME, NON_UNIQUE
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = 'belajar_fk' AND TABLE_NAME = 'produk'
ORDER BY INDEX_NAME, SEQ_IN_INDEX;Output yang Dihasilkan:
+--------------------+-------------+------------+
| INDEX_NAME | COLUMN_NAME | NON_UNIQUE |
+--------------------+-------------+------------+
| fk_produk_kategori | kategori_id | 1 |
| PRIMARY | id | 0 |
+--------------------+-------------+------------+
2 rows in set (0.00 sec)Hasilnya memuat dua index: PRIMARY untuk kolom id, dan index bernama fk_produk_kategori (nilai NON_UNIQUE = 1) yang dibuat otomatis oleh foreign key. Karena constraint kita diberi nama, index otomatisnya ikut memakai nama tersebut; bila constraint tidak dinamai, index otomatis memakai nama kolom anaknya. Definisi lengkap tabel anak termasuk klausa FK-nya bisa dilihat dengan SHOW CREATE TABLE:
SHOW CREATE TABLE produk\GOutput yang Dihasilkan:
*************************** 1. row ***************************
Table: produk
Create Table: CREATE TABLE `produk` (
`id` int NOT NULL,
`nama` varchar(100) NOT NULL,
`harga` decimal(10,2) NOT NULL,
`kategori_id` int DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `fk_produk_kategori` (`kategori_id`),
CONSTRAINT `fk_produk_kategori` FOREIGN KEY (`kategori_id`) REFERENCES `kategori` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)Baris CONSTRAINT fk_produk_kategori FOREIGN KEY (kategori_id) REFERENCES kategori (id) ON DELETE CASCADE pada hasil di atas adalah bentuk final definisi FK kita — kalimat inilah yang menjadi rujukan bila struktur relasi perlu diperiksa atau dibandingkan antar lingkungan.
Aksi ON DELETE CASCADE #
Aksi CASCADE berarti: bila induk dihapus, seluruh anaknya ikut dihapus. Kategori Buku (id 2) memiliki satu produk; kita hapus kategorinya dan amati produknya:
DELETE FROM kategori WHERE id = 2;
SELECT id, nama, harga, kategori_id FROM produk ORDER BY id;Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)
+----+---------------+---------+-------------+
| id | nama | harga | kategori_id |
+----+---------------+---------+-------------+
| 1 | Buku Tulis 58 | 4500.00 | 1 |
+----+---------------+---------+-------------+
1 row in set (0.00 sec)Satu statement DELETE pada tabel induk menghapus dua hal sekaligus: baris kategori Buku dan baris produk Laskar Pelangi yang menempel padanya. Tabel produk kini hanya menyisakan Buku Tulis 58. Inilah kekuatan sekaligus bahaya CASCADE — penghematan kerja saat relasi memang seumur hidup (item pesanan ikut pesanannya), tetapi juga lubang penghapusan massal yang tidak disadari.
Aksi ON DELETE RESTRICT #
Aksi RESTRICT adalah kebalikannya: penghapusan induk ditolak selama masih ada anak yang merujuk. Kita buat tabel pembelian yang mencatat penjualan — riwayat keuangan yang tidak boleh kehilangan barang rujukannya:
CREATE TABLE pembelian (
id INT PRIMARY KEY,
produk_id INT NOT NULL,
jumlah INT NOT NULL,
tanggal DATE NOT NULL,
CONSTRAINT fk_pembelian_produk FOREIGN KEY (produk_id)
REFERENCES produk (id)
ON DELETE RESTRICT
);
INSERT INTO pembelian (id, produk_id, jumlah, tanggal) VALUES
(1, 1, 2, '2026-01-15');Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 1 row affected (0.00 sec)Sekarang kita coba menghapus produk yang sudah tercatat terjual:
-- @error
DELETE FROM produk WHERE id = 1;Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`belajar_fk`.`pembelian`, CONSTRAINT `fk_pembelian_produk` FOREIGN KEY (`produk_id`) REFERENCES `produk` (`id`) ON DELETE RESTRICT)
ByeGalat ERROR 1451 (23000) — Cannot delete or update a parent row: a foreign key constraint fails — menahan baris produk di tempatnya. Perhatikan simetrinya dengan galat 1452: anak tanpa induk ditolak saat masuk (1452), induk dengan anak ditolak saat dihapus (1451). Produk tetap utuh, dan riwayat pembelian tetap punya rujukan yang sah. RESTRICT juga merupakan perilaku bawaan bila klausa ON DELETE tidak ditulis sama sekali.
Aksi ON DELETE SET NULL #
Aksi ketiga, SET NULL, memilih jalan tengah: induk boleh dihapus, dan kolom rujukan pada anak dijadikan NULL. Pola ini cocok untuk data pelengkap — misalnya ulasan yang tetap layak dibaca walau produknya sudah tidak dijual. Syaratnya satu: kolom anak harus mengizinkan NULL:
CREATE TABLE ulasan (
id INT PRIMARY KEY,
isi VARCHAR(200) NOT NULL,
produk_id INT,
CONSTRAINT fk_ulasan_produk FOREIGN KEY (produk_id)
REFERENCES produk (id)
ON DELETE SET NULL
);
INSERT INTO ulasan (id, isi, produk_id) VALUES
(1, 'Kertasnya tebal dan rapi', 1),
(2, 'Harga bersahabat untuk pelajar', 1);Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0Baris pembelian kita hapus lebih dulu supaya RESTRICT tidak menghalangi, kemudian produknya dihapus:
DELETE FROM pembelian WHERE id = 1;
DELETE FROM produk WHERE id = 1;
SELECT id, isi, produk_id FROM ulasan ORDER BY id;Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)
Query OK, 1 row affected (0.00 sec)
+----+--------------------------------+-----------+
| id | isi | produk_id |
+----+--------------------------------+-----------+
| 1 | Kertasnya tebal dan rapi | NULL |
| 2 | Harga bersahabat untuk pelajar | NULL |
+----+--------------------------------+-----------+
2 rows in set (0.00 sec)Kedua ulasan tetap ada, tetapi kolom produk_id kini bernilai NULL — produk rujukannya sudah tiada. Bandingkan ketiga aksi tersebut dalam satu tabel:
Aksi ON DELETE | Induk dihapus | Kondisi kolom anak |
|---|---|---|
RESTRICT | ditolak bila masih ada anak | — |
CASCADE | berhasil | baris ikut terhapus |
SET NULL | berhasil | kolom rujukan jadi NULL (kolom harus nullable) |
Relasi antar tabel di belajar_fk kini dijaga penuh oleh tiga foreign key dengan tiga kebijakan penghapusan yang berbeda. Sebelum lanjut ke penjaga keunikan yang lebih ringan — UNIQUE Constraint — kita bereskan database latihan:
DROP DATABASE IF EXISTS belajar_fk;Output yang Dihasilkan:
Query OK, 4 rows affected (0.00 sec)Angka rows affected pada respons DROP DATABASE di atas sama dengan jumlah tabel yang dibuang — di sini empat tabel (kategori, produk, pembelian, ulasan) beserta seluruh constraint di dalamnya.