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:

sql
CREATE DATABASE belajar_fk;
USE belajar_fk;

Output yang Dihasilkan:

text
Query OK, 1 row affected (0.00 sec)

Database changed
sql
CREATE 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:

text
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:

  1. Baris kategori_id INT: Kolom anak bertipe INT — wajib sama tipe dengan kategori.id yang menjadi rujukannya. Kolom ini sengaja dibiarkan boleh NULL agar produk tanpa kategori tetap sah.
  2. Baris CONSTRAINT fk_produk_kategori: Nama constraint kita pilih sendiri agar mudah dibaca di pesan galat; tanpa nama, server mengarang nama otomatis seperti produk_ibfk_1.
  3. Kata kunci FOREIGN KEY (kategori_id): Menandai kategori_id sebagai kolom kunci asing pada tabel anak.
  4. Klausa REFERENCES kategori (id): Menyatakan tabel induk (kategori) beserta kolom yang dirujuk (id). Nilai kategori_id hanya sah bila id tersebut ada di tabel induk — atau bernilai NULL.
  5. 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:

sql
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:

text
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:

sql
-- @error
INSERT INTO produk (id, nama, harga, kategori_id) VALUES (3, 'Spidol Whiteboard', 18000.00, 9);

Output yang Dihasilkan:

text
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)
Bye

Server 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:

sql
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:

text
+--------------------+-------------+------------+
| 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:

sql
SHOW CREATE TABLE produk\G

Output yang Dihasilkan:

text
*************************** 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:

sql
DELETE FROM kategori WHERE id = 2;

SELECT id, nama, harga, kategori_id FROM produk ORDER BY id;

Output yang Dihasilkan:

text
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:

sql
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:

text
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:

sql
-- @error
DELETE FROM produk WHERE id = 1;

Output yang Dihasilkan:

text
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)
Bye

Galat 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:

sql
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:

text
Query OK, 0 rows affected (0.00 sec)

Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

Baris pembelian kita hapus lebih dulu supaya RESTRICT tidak menghalangi, kemudian produknya dihapus:

sql
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:

text
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 DELETEInduk dihapusKondisi kolom anak
RESTRICTditolak bila masih ada anak—
CASCADEberhasilbaris ikut terhapus
SET NULLberhasilkolom 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:

sql
DROP DATABASE IF EXISTS belajar_fk;

Output yang Dihasilkan:

text
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.

Referensi #