Perbedaan Primary Key dan Foreign Key di MySQL: Panduan Lengkap

primary key dan foreign key adalah dua konsep yang hampir selalu muncul barengan begitu kita bicara database relasional. kalau primary key itu identitas, foreign key itu penghubungnya. tanpa keduanya, tabel cuma jadi kumpulan data yang tidak saling kenal.
dulu saya sempat menganggap foreign key cuma formalitas. bikin tabel, relasi dijaga lewat kode Laravel saja. sampai suatu hari laporan keuangan menunjukkan transaksi milik mahasiswa dengan NIM yang tidak ada di tabel mahasiswa. data yatim. sejak itu saya tidak pernah merancang tabel tanpa foreign key lagi. artikel ini saya tulis supaya kalian tidak perlu mengalami hal yang sama.
ringkasan cepat: primary key = identitas unik tiap baris, satu per tabel, tidak boleh NULL. foreign key = kolom yang merujuk ke primary key di tabel lain, boleh duplikat dan boleh NULL, tujuannya menjaga integritas data antar tabel.
1. Apa Itu Primary Key?
primary key (kunci utama) adalah kolom atau kombinasi kolom yang secara unik mengidentifikasi setiap baris dalam tabel. di InnoDB, primary key sekaligus menentukan urutan penyimpanan fisik data (clustered index), jadi pemilihan primary key ngaruh langsung ke performa.
Karakteristik primary key
- unik — tidak boleh ada nilai yang sama
- NOT NULL — wajib diisi
- hanya satu per tabel — tapi bisa composite (gabungan beberapa kolom)
- otomatis jadi clustered index di InnoDB, jadi pencarian berdasarkan primary key itu paling cepat
- tipe stabil — jangan pakai kolom yang sering berubah seperti nama atau alamat
Contoh paling umum: surrogate key
CREATE TABLE mahasiswa (
nim VARCHAR(10) PRIMARY KEY,
nama VARCHAR(100) NOT NULL,
jurusan VARCHAR(50)
) ENGINE=InnoDB;
INSERT INTO mahasiswa VALUES
('G64180001', 'Andi Pratama', 'Ilmu Komputer'),
('G64180002', 'Budi Santoso', 'Sistem Informasi');
di contoh ini nim adalah natural key (kunci alami dari dunia nyata). untuk tabel yang tidak punya natural key yang stabil, kita biasa pakai surrogate key:
CREATE TABLE users (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE
) ENGINE=InnoDB;
BIGINT UNSIGNED AUTO_INCREMENT adalah standar paling aman di 2026. INT biasa maksimal 2,1 miliar, sementara BIGINT UNSIGNED sampai 18 quintillion, jadi tidak akan habis untuk aplikasi jangka panjang.
Composite primary key
kalau satu kolom tidak cukup untuk unik, gabungkan dua kolom:
CREATE TABLE nilai (
nim VARCHAR(10) NOT NULL,
kode_mk VARCHAR(10) NOT NULL,
nilai INT,
PRIMARY KEY (nim, kode_mk)
) ENGINE=InnoDB;
artinya kombinasi nim + kode_mk harus unik. satu mahasiswa tidak boleh punya dua nilai untuk mata kuliah yang sama. ini pola klasik untuk tabel pivot.
2. Apa Itu Foreign Key?
foreign key adalah kolom di tabel anak yang merujuk ke primary key (atau unique key) di tabel induk. tugasnya cuma satu: menjaga referential integrity, memastikan setiap nilai di kolom anak memang ada di tabel induk.
Karakteristik foreign key
- boleh NULL jika relasinya opsional
- boleh duplikat — inilah yang bikin relasi satu-ke-banyak (one-to-many) bisa terjadi
- boleh banyak per tabel — satu tabel bisa punya beberapa foreign key ke tabel berbeda
- harus tipe data identik dengan kolom yang dirujuk (INT ke INT, BIGINT ke BIGINT, panjang dan charset VARCHAR harus sama)
- hanya jalan di InnoDB — MyISAM akan mengabaikan definisi foreign key tanpa error yang jelas
- butuh index — MySQL otomatis bikinkan index jika belum ada, tapi lebih baik definisikan eksplisit
Contoh dasar
CREATE TABLE transaksi (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
nim VARCHAR(10),
total DECIMAL(12,2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (nim) REFERENCES mahasiswa(nim)
ON DELETE RESTRICT
ON UPDATE CASCADE
) ENGINE=InnoDB;
-- berhasil, karena G64180001 ada di mahasiswa
INSERT INTO transaksi (nim, total) VALUES ('G64180001', 500000);
-- gagal, karena G64180099 tidak ada di mahasiswa
-- ERROR 1452: Cannot add or update a child row: a foreign key constraint fails
INSERT INTO transaksi (nim, total) VALUES ('G64180099', 300000);
tanpa foreign key, insert kedua akan lolos dan jadi data yatim. dengan foreign key, MySQL langsung menolak.
Cara menambah foreign key ke tabel yang sudah ada
-- pastikan kolom sudah di-index dan tipenya sama
ALTER TABLE transaksi
ADD CONSTRAINT fk_transaksi_mahasiswa
FOREIGN KEY (nim) REFERENCES mahasiswa(nim)
ON DELETE RESTRICT ON UPDATE CASCADE;
-- cek foreign key yang sudah ada
SELECT CONSTRAINT_NAME, TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = DATABASE() AND REFERENCED_TABLE_NAME IS NOT NULL;
-- hapus jika salah
ALTER TABLE transaksi DROP FOREIGN KEY fk_transaksi_mahasiswa;
di Laravel 11/12, syntax modernnya lebih ringkas:
Schema::create('transaksi', function (Blueprint $table) {
$table->id();
$table->string('nim', 10)->nullable();
$table->decimal('total', 12, 2);
$table->timestamps();
$table->foreign('nim')->references('nim')->on('mahasiswa')
->restrictOnDelete()->cascadeOnUpdate();
// atau cara pendek:
// $table->foreignId('user_id')->constrained()->restrictOnDelete();
});
3. Tabel Perbandingan Lengkap
| Aspek | Primary Key | Foreign Key |
|---|---|---|
| Tujuan | identitas unik tiap baris | menghubungkan tabel |
| Keunikan | wajib unik | boleh duplikat |
| NULL | tidak boleh | boleh (jika relasi opsional) |
| Jumlah per tabel | maksimal 1 (bisa composite) | boleh banyak |
| Index | otomatis clustered index | otomatis dibuatkan index jika belum ada, tapi sebaiknya buat eksplisit |
| Wajib ada? | sangat disarankan, hampir wajib | opsional tapi best practice |
| Merujuk tabel lain? | tidak | ya, ke PK/UNIQUE di tabel induk |
| Contoh | id, nim, kode_mk | transaksi.nim, orders.user_id |
kalau masih bingung, ingat analoginya: primary key itu KTP, satu orang satu nomor. foreign key itu kolom “nama ayah” di kartu keluarga yang merujuk ke KTP ayah, banyak anak bisa merujuk ke ayah yang sama.
4. Contoh Relasi Lengkap 4 Tabel
ini contoh yang saya pakai di kelas database, sudah mencakup one-to-many dan many-to-many:
-- Tabel induk 1
CREATE TABLE jurusan (
id_jurusan INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
nama_jurusan VARCHAR(50) NOT NULL UNIQUE
) ENGINE=InnoDB;
-- Tabel induk 2, FK ke jurusan (one-to-many)
CREATE TABLE mahasiswa (
nim VARCHAR(10) PRIMARY KEY,
nama VARCHAR(100) NOT NULL,
id_jurusan INT UNSIGNED,
FOREIGN KEY (id_jurusan) REFERENCES jurusan(id_jurusan)
ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB;
-- Tabel induk 3
CREATE TABLE mata_kuliah (
kode_mk VARCHAR(10) PRIMARY KEY,
nama_mk VARCHAR(100) NOT NULL,
sks TINYINT UNSIGNED
) ENGINE=InnoDB;
-- Tabel pivot, composite PK + dua FK (many-to-many)
CREATE TABLE nilai (
nim VARCHAR(10) NOT NULL,
kode_mk VARCHAR(10) NOT NULL,
nilai TINYINT UNSIGNED,
PRIMARY KEY (nim, kode_mk),
FOREIGN KEY (nim) REFERENCES mahasiswa(nim) ON DELETE CASCADE,
FOREIGN KEY (kode_mk) REFERENCES mata_kuliah(kode_mk) ON DELETE RESTRICT
) ENGINE=InnoDB;
Diagram relasi
jurusan 1──< mahasiswa 1──< nilai >──1 mata_kuliah
(satu jurusan punya banyak mahasiswa, satu mahasiswa punya banyak nilai)
Query JOIN dengan PK dan FK
-- transaksi lengkap dengan nama mahasiswa
SELECT t.id, m.nama, m.nim, t.total
FROM transaksi t
JOIN mahasiswa m ON t.nim = m.nim;
-- rekap nilai dengan nama mahasiswa dan mata kuliah
SELECT m.nama, mk.nama_mk, n.nilai
FROM nilai n
JOIN mahasiswa m ON n.nim = m.nim
JOIN mata_kuliah mk ON n.kode_mk = mk.kode_mk
WHERE n.nilai >= 70
ORDER BY m.nama;
-- cek mahasiswa yang belum punya transaksi (LEFT JOIN)
SELECT m.nim, m.nama
FROM mahasiswa m
LEFT JOIN transaksi t ON m.nim = t.nim
WHERE t.nim IS NULL;
tanpa foreign key dan index yang benar, JOIN di atas bisa lambat di data besar. dengan foreign key yang sudah ter-index, optimizer MySQL bisa pakai index dengan efisien.
5. ON DELETE dan ON UPDATE: Jangan Asal CASCADE
ini bagian yang paling sering bikin kecelakaan. aturannya dipasang saat definisi foreign key:
FOREIGN KEY (nim) REFERENCES mahasiswa(nim)
ON DELETE CASCADE
ON UPDATE CASCADE
| Aksi | Efek saat data induk dihapus/diupdate | Kapan dipakai |
|---|---|---|
CASCADE | data anak ikut terhapus/terupdate otomatis | relasi yang memang dependen, misal order_items ikut hapus saat orders dihapus |
SET NULL | kolom FK di anak jadi NULL | relasi opsional, misal mahasiswa.id_jurusan jadi NULL kalau jurusan dihapus |
RESTRICT | tolak penghapusan jika masih ada data anak | paling aman untuk data penting, misal tolak hapus mahasiswa jika masih ada transaksi |
NO ACTION | sama seperti RESTRICT di MySQL (cek di akhir statement) | sama dengan RESTRICT |
SET DEFAULT | di MySQL di-parse tapi tidak didukung InnoDB | jangan dipakai |
pengalaman saya: jangan pakai CASCADE untuk relasi ke tabel master yang penting. saya pernah lihat satu DELETE FROM users WHERE id=5 menghapus ribuan baris di 6 tabel anak karena semua pakai CASCADE. untuk data transaksi, RESTRICT jauh lebih selamat, biarkan error muncul daripada data hilang diam-diam.
pola modern 2026: banyak tim sekarang pilih
RESTRICT+ soft delete (deleted_at) di aplikasi. jadi data tidak benar-benar dihapus, cuma ditandai, dan foreign key tetap konsisten. baca juga perbedaan DELETE dan TRUNCATE untuk paham kenapaTRUNCATEakan gagal jika masih ada foreign key.
Self-referencing foreign key
untuk struktur hirarki seperti kategori:
CREATE TABLE kategori (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
nama VARCHAR(100) NOT NULL,
parent_id INT UNSIGNED NULL,
FOREIGN KEY (parent_id) REFERENCES kategori(id) ON DELETE SET NULL
) ENGINE=InnoDB;
6. Update 2026: Pilih Primary Key yang Tepat
konsep PK dan FK tidak berubah, tapi cara kita memilih tipe primary key di 2026 sudah beda dibanding 5 tahun lalu.
MySQL 8.4 LTS jadi standar
sejak April 2026, MySQL 8.0 sudah End-of-Life. untuk project baru, pakai MySQL 8.4 LTS (didukung sampai 2032) atau MySQL 9.x Innovation. syntax foreign key tetap sama, tapi default authentication dan sql_mode lebih ketat. kalau tutorial lama kalian masih pakai MySQL 5.7, waktunya upgrade, karena 5.7 sudah tidak dapat security patch.
AUTO_INCREMENT vs UUIDv7 vs ULID
ini pertanyaan yang paling sering muncul di 2026:
| Tipe PK | Kelebihan | Kekurangan | Cocok untuk |
|---|---|---|---|
| BIGINT UNSIGNED AUTO_INCREMENT | paling cepat, paling hemat storage (8 byte), urut, tidak fragmentasi index | bisa ditebak, tidak cocok untuk distributed DB | monolit, single database, 90% aplikasi UMKM/startup |
| UUIDv4 | global unik, tidak bisa ditebak | acak, bikin clustered index terfragmentasi, lambat di data besar | jangan dipakai untuk PK di 2026 |
| UUIDv7 | global unik + time-ordered (urut waktu), tidak fragmentasi, standar baru IETF 2024 | 16 byte (2x BIGINT), string lebih panjang | microservice, multi-region, sistem yang butuh ID tidak tebak (order_id, invoice) |
| ULID / Snowflake ID | mirip UUIDv7, sortable, bisa generate di aplikasi | butuh library tambahan | alternatif UUIDv7, dipakai di Discord/Twitter pattern |
rekomendasi praktis saya di 2026:
- untuk
users,posts,transactionsdi aplikasi biasa, tetap pakaiBIGINT UNSIGNED AUTO_INCREMENT. simpel, cepat, dan semua ORM (Laravel, Prisma, Drizzle) optimasinya sudah matang. - untuk
orders,payments, atau sistem yang butuh ID publik yang tidak bisa ditebak, pakai UUIDv7. di MySQL simpan sebagaiBINARY(16)biar hemat, bukanVARCHAR(36):-- MySQL 8.0.13+ punya fungsi UUID_TO_BIN / BIN_TO_UUID CREATE TABLE orders ( id BINARY(16) PRIMARY KEY DEFAULT (UUID_TO_BIN(UUID())), user_id BIGINT UNSIGNED NOT NULL, total DECIMAL(12,2), FOREIGN KEY (user_id) REFERENCES users(id) ) ENGINE=InnoDB; - jangan pakai UUIDv4 acak sebagai PK kalau data sudah jutaan baris. insert akan lompat-lompat di B-Tree dan bikin performa drop.
InnoDB wajib, MyISAM tinggalkan
ini masih relevan di 2026 karena saya masih menemukan project lama pakai MyISAM. ingat: MyISAM tidak mendukung foreign key, CREATE TABLE ... FOREIGN KEY akan sukses tapi constraint diabaikan. selalu cek:
SHOW CREATE TABLE mahasiswa; -- lihat ENGINE=InnoDB
SHOW TABLE STATUS WHERE Name='mahasiswa';
kalau masih MyISAM, migrasi dulu. detailnya baca perbedaan MyISAM dan InnoDB.
Foreign key di era serverless & ORM
- Prisma / Drizzle / Eloquent di 2026 sudah generate foreign key otomatis dari schema. di Prisma cukup
user User @relation(fields: [userId], references: [id], onDelete: Cascade), migrasi akan bikin FK-nya. jangan dimatikan hanya karena “ribet”. - PlanetScale / Vitess: dulu terkenal tidak support foreign key, tapi sejak akhir 2023 Vitess sudah support FK secara native dan PlanetScale mengaktifkannya di 2024. jadi tidak ada alasan lagi skip FK karena pakai branching database.
- Index otomatis: di MySQL 8.x, kalau kolom FK belum ada index, MySQL otomatis buatkan. tapi tetap definisikan index secara eksplisit biar nama index jelas dan explain plan lebih mudah dibaca.
7. Cara Cek, Validasi, dan Debug FK
-- lihat semua FK di database aktif
SELECT TABLE_NAME, CONSTRAINT_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = DATABASE() AND REFERENCED_TABLE_NAME IS NOT NULL;
-- cek relasi yang orphan (seharusnya 0 baris kalau FK benar)
SELECT t.nim FROM transaksi t
LEFT JOIN mahasiswa m ON t.nim = m.nim
WHERE t.nim IS NOT NULL AND m.nim IS NULL;
-- nonaktifkan sementara untuk import bulk (hati-hati, jangan lupa aktifkan lagi)
SET FOREIGN_KEY_CHECKS = 0;
-- ... import data ...
SET FOREIGN_KEY_CHECKS = 1;
-- tambah FK tanpa lock lama (MySQL 8.0+ online DDL)
ALTER TABLE transaksi
ADD CONSTRAINT fk_transaksi_mahasiswa FOREIGN KEY (nim) REFERENCES mahasiswa(nim),
ALGORITHM=INPLACE, LOCK=NONE;
FOREIGN_KEY_CHECKS=0 berguna saat restore dump, tapi jangan dipakai di aplikasi harian. kalau lupa diaktifkan lagi, data yatim bisa masuk tanpa warning.
8. Kesalahan Umum Pemula
- tipe data tidak sama persis —
INTdi induk tapiBIGINTdi anak, atauVARCHAR(10)vsVARCHAR(20), collation beda. MySQL akan errorerrno 150. - pakai MyISAM — lupa
ENGINE=InnoDB, FK diam-diam tidak aktif. - lupa index di kolom FK — walau MySQL auto-create, di tabel besar alter bisa lambat.
- CASCADE sembrono — hapus satu user, semua transaksi, komentar, dan log ikut hilang.
- natural key yang sering berubah — pakai
emailsebagai PK lalu email diubah, semua FK harus CASCADE update, berat. - tidak kasih nama constraint — biar
DROP FOREIGN KEYmudah, selalu kasih namafk_anak_induk.
9. Primary Key vs Unique Key vs Foreign Key
| Aspek | Primary Key | Unique Key | Foreign Key |
|---|---|---|---|
| NULL | tidak boleh | boleh satu NULL (MySQL) | boleh |
| Jumlah per tabel | 1 | banyak | banyak |
| Index otomatis | ya (clustered) | ya (unique) | auto dibuat jika belum ada |
| Fungsi | identitas baris | cegah duplikat | jaga relasi |
| Contoh | users.id | users.email | orders.user_id |
ingat: UNIQUE cocok untuk email, username, kode_voucher. PRIMARY KEY untuk id. FOREIGN KEY untuk user_id yang merujuk ke users(id).
10. Checklist Best Practice 2026
- pakai
BIGINT UNSIGNED AUTO_INCREMENTuntuk PK internal,UUIDv7 BINARY(16)untuk ID publik/terdistribusi - selalu
ENGINE=InnoDB, cekSHOW CREATE TABLE - kasih nama constraint yang jelas:
fk_transaksi_mahasiswa,fk_nilai_mk - pakai
RESTRICTsebagai default,CASCADEhanya jika paham dampaknya - untuk data penting, pertimbangkan soft delete (
deleted_at) daripada hard delete - samakan tipe, panjang, charset, dan collation antara FK dan PK yang dirujuk
- buat index eksplisit untuk FK, jangan andalkan auto-create saja
- test migrasi di staging dengan data jutaan baris sebelum ke production, terutama
ALTER TABLE ... ADD FOREIGN KEY
Kesimpulan
- primary key adalah identitas unik tiap baris, satu per tabel, NOT NULL, dan jadi clustered index. pilih
BIGINT AUTO_INCREMENTuntuk monolit,UUIDv7untuk sistem terdistribusi. - foreign key adalah penghubung antar tabel yang menjaga integritas data. boleh duplikat, boleh NULL, dan boleh banyak per tabel.
- kombinasi keduanya memungkinkan JOIN yang cepat dan mencegah data yatim. tanpa FK, aplikasi harus jaga relasi manual dan itu sumber bug paling sering.
- di MySQL 2026, selalu pakai InnoDB, pahami
ON DELETEdengan hati-hati, dan manfaatkan fitur modern sepertiUUID_TO_BINdan online DDL. untuk pondasi yang lebih dalam, lanjutkan ke perbedaan MyISAM dan InnoDB, perbedaan DELETE dan TRUNCATE, dan perbedaan trigger dan stored procedure biar pemahaman database kalian bulat.
Baca Juga Mengenai :
Pertanyaan yang Sering Diajukan
Apa itu primary key?
Primary key adalah kolom atau kombinasi kolom yang secara unik mengidentifikasi setiap baris di tabel. nilainya harus unik dan tidak boleh NULL, hanya boleh satu per tabel, dan otomatis jadi clustered index di InnoDB.
Apa fungsi foreign key?
Foreign key menghubungkan tabel anak ke tabel induk dengan merujuk primary key atau unique key di tabel lain. fungsinya menjaga referential integrity agar tidak ada data yatim, misal transaksi yang merujuk mahasiswa yang sudah tidak ada.
Apakah satu tabel bisa punya lebih dari satu primary key?
Tidak. satu tabel hanya boleh punya satu primary key, tapi primary key itu boleh composite (gabungan beberapa kolom). jika butuh keunikan lain, pakai UNIQUE KEY.
Apakah foreign key wajib dibuat?
Secara teknis tidak wajib, tapi sangat disarankan. tanpa foreign key, validasi relasi harus dilakukan manual di aplikasi dan rawan bug. di 2026 hampir semua project production pakai foreign key dengan engine InnoDB.
Apa beda primary key dengan unique key?
Primary key tidak boleh NULL dan hanya satu per tabel, otomatis clustered index. unique key boleh satu NULL (di MySQL), boleh banyak per tabel, dan fungsinya mencegah duplikat bukan sebagai identitas utama.
Untuk primary key di 2026 lebih baik AUTO_INCREMENT atau UUIDv7?
Untuk aplikasi monolit single database, BIGINT UNSIGNED AUTO_INCREMENT masih paling simpel dan cepat. untuk sistem terdistribusi, microservice, atau butuh ID yang tidak bisa ditebak, UUIDv7 (time-ordered) jadi standar baru 2026 karena tidak bikin index fragmentation seperti UUIDv4.
Kapan pakai ON DELETE CASCADE dan kapan RESTRICT?
Pakai CASCADE hanya untuk data anak yang memang harus ikut hilang, misal item_order ikut terhapus saat order dihapus. untuk data penting, pakai RESTRICT atau NO ACTION biar MySQL menolak penghapusan dan mencegah kehilangan data masal tanpa sengaja.
Kenapa foreign key saya error saat CREATE TABLE?
Penyebab paling umum: engine masih MyISAM (tidak support FK), tipe data kolom FK dan PK tidak sama persis (INT vs BIGINT), atau collation/charset beda. pastikan dua-duanya InnoDB, tipe dan panjang sama, dan tabel induk sudah ada sebelum tabel anak.

