TL;DR
Primary key adalah kolom yang nandain tiap baris di satu tabel secara unik. Nilainya gak boleh kembar dan gak boleh kosong. Foreign key adalah kolom di tabel lain yang nunjuk ke primary key itu, dan inilah yang bikin dua tabel nyambung. Contohnya id_pelanggan jadi primary key di tabel pelanggan, lalu muncul lagi sebagai foreign key di tabel transaksi supaya database tau transaksi ini punya siapa.
Primary key adalah kolom yang nandain tiap baris di satu tabel secara unik. Nilainya gak boleh kembar, gak boleh kosong.
Foreign key adalah kolom di tabel lain yang nunjuk ke primary key itu. Inilah yang bikin dua tabel nyambung.
Tanpa dua hal ini, database kamu cuma kumpulan spreadsheet yang kebetulan disimpan di server. JOIN gak jalan, data ganda numpuk, dan transaksi bisa nyantol ke pelanggan yang gak pernah ada.
Primary key adalah kolom (atau gabungan kolom) yang nilainya unik di seluruh tabel dan gak pernah kosong. Database pakai kolom ini buat nunjuk satu baris tertentu dengan pasti, tanpa perlu nebak baris mana yang dimaksud.
Satu tabel cuma boleh punya satu primary key. Ini aturan mati.
CREATE TABLE pelanggan (
id_pelanggan INT PRIMARY KEY,
nama VARCHAR(100) NOT NULL,
kota VARCHAR(50),
email VARCHAR(100) UNIQUE
);
Begitu id_pelanggan jadi primary key, database otomatis nolak dua hal: baris dengan id yang sama, dan baris dengan id kosong.
Coba masukin data kembar:
INSERT INTO pelanggan VALUES (1, 'Rina', 'Surabaya', 'rina@example.com');
INSERT INTO pelanggan VALUES (1, 'Budi', 'Medan', 'budi@example.com');
-- ERROR: duplicate key value violates unique constraint
Ditolak. Bagus, itu memang tugasnya.
Kolom yang nilainya gak bakal berubah seumur hidup barisnya. Ini kriteria utamanya.
Kandidat yang bagus: id numerik yang naik otomatis, atau UUID.
Kandidat yang jelek: nomor HP (bisa ganti), email (bisa ganti), nama (bisa kembar). Aku pernah lihat sistem yang pakai email sebagai primary key. Begitu satu pelanggan ganti email, semua transaksi lamanya jadi nyantol ke orang yang salah.
Kadang gak ada satu kolom pun yang unik sendirian. Contohnya tabel detail transaksi:
CREATE TABLE detail_transaksi (
id_transaksi INT,
id_produk INT,
qty INT NOT NULL,
PRIMARY KEY (id_transaksi, id_produk)
);
id_transaksi muncul berkali-kali (satu transaksi bisa beli 5 produk). id_produk juga muncul berkali-kali. Tapi kombinasi keduanya unik. Satu produk cuma boleh muncul sekali di satu transaksi.
Foreign key adalah kolom di satu tabel yang nilainya wajib ada di primary key tabel lain. Fungsinya nyambungin baris di tabel anak ke baris induknya, dan database yang jagain supaya sambungan itu gak pernah putus.
CREATE TABLE transaksi (
id_transaksi INT PRIMARY KEY,
id_pelanggan INT NOT NULL,
tanggal DATE NOT NULL,
total DECIMAL(12,2),
CONSTRAINT fk_transaksi_pelanggan
FOREIGN KEY (id_pelanggan)
REFERENCES pelanggan(id_pelanggan)
);
Sekarang coba masukin transaksi buat pelanggan nomor 99, padahal tabel pelanggan cuma punya id 1, 2, dan 3:
INSERT INTO transaksi VALUES (105, 99, '2026-01-10', 50000);
-- ERROR: violates foreign key constraint
Ditolak juga. Ini yang disebut referential integrity: database mastiin tiap transaksi punya pemilik yang beneran ada.
Tanpa foreign key, kamu bisa punya data yatim: transaksi yang nyantol ke pelanggan nomor 99 yang gak pernah ada di sistem.
Kelihatannya sepele. Sampai kamu jalanin laporan penjualan per pelanggan, dan Rp 3,4 juta hilang begitu aja karena baris-barisnya gak ketemu induknya waktu di-JOIN.
Kalau kamu curiga ada data yatim di database yang FK-nya belum dipasang, cek pakai ini:
SELECT t.id_transaksi, t.id_pelanggan
FROM transaksi t
LEFT JOIN pelanggan p ON t.id_pelanggan = p.id_pelanggan
WHERE p.id_pelanggan IS NULL;
Baris yang keluar itu transaksi tanpa pemilik. Logika LEFT JOIN-nya dijelasin lengkap di halaman glosarium JOIN.
Begitu relasinya kepasang, JOIN jadi gampang. Ini contoh dari dataset toko_berkah:
SELECT p.nama,
p.kota,
COUNT(t.id_transaksi) AS jumlah_transaksi,
SUM(t.total) AS total_belanja
FROM pelanggan p
JOIN transaksi t ON p.id_pelanggan = t.id_pelanggan
GROUP BY p.nama, p.kota
ORDER BY total_belanja DESC;
Bagian ON p.id_pelanggan = t.id_pelanggan itu persis relasi yang tadi kamu bikin. Primary key di kiri, foreign key di kanan.
Detail sintaks INNER JOIN-nya ada di halaman glosarium JOIN.
Waktu aku pertama kali nge-cek dataset toko_berkah (3.180 transaksi dari 6 bulan), tabel transaksinya belum punya foreign key sama sekali. Data dimasukin lewat impor CSV dari kasir.
Query cek data yatim di atas nemuin 47 transaksi dengan id_pelanggan yang gak ada di tabel pelanggan. Totalnya Rp 6,2 juta.
Penyebabnya: 47 transaksi itu dari pembeli yang batal daftar akun. Kasir tetap masukin id sementara, tapi barisnya gak pernah nyampe tabel pelanggan.
Dampaknya di laporan bulanan: total omzet dari query SUM(total) di tabel transaksi adalah Rp 214 juta. Tapi laporan penjualan per pelanggan, yang pakai JOIN, cuma nunjukin Rp 207,8 juta.
Selisih 2,9%. Cukup kecil buat gak keliatan, cukup besar buat bikin salah keputusan.
Setelah foreign key dipasang, baris yatim baru gak bisa masuk lagi. 47 baris lama? Itu urusan lain. Harus dibersihin manual dulu sebelum ALTER TABLE mau jalan.
Waktu bikin foreign key, kamu bisa atur apa yang terjadi kalau baris induknya dihapus.
| Aturan | Efek waktu induk dihapus | Kapan dipakai |
|---|---|---|
| ON DELETE RESTRICT | Penghapusan ditolak selama masih ada anak | Data transaksi dan keuangan |
| ON DELETE CASCADE | Semua baris anak ikut kehapus | Data turunan yang gak berharga sendiri |
| ON DELETE SET NULL | Foreign key anak jadi kosong | Relasi opsional, misalnya kategori produk |
CASCADE itu praktis dan bahaya. Hapus satu pelanggan, semua riwayat transaksinya lenyap, termasuk yang udah masuk laporan pajak.
Buat tabel transaksi, pakai RESTRICT. Database bakal nolak penghapusan, dan kamu dipaksa mikir dulu sebelum ngapus.
Email, nomor HP, NIK yang salah ketik. Semua bisa berubah. Begitu berubah, semua relasi ke tabel lain putus. Pakai id yang gak punya arti apa-apa selain jadi penanda.
Primary key otomatis dapat index. Foreign key nggak di sebagian besar database: MySQL bikin otomatis, PostgreSQL nggak.
Akibatnya JOIN jadi lambat banget di tabel besar. Bikin index manual:
CREATE INDEX idx_transaksi_pelanggan ON transaksi(id_pelanggan);
INT di tabel induk, VARCHAR di tabel anak. Database bakal nolak bikin constraintnya, dan pesan errornya sering bikin bingung.
Udah dibahas di atas. Sekali kehapus, gak ada undo.
Iya, ada biaya pengecekan tiap kali insert. Tapi biayanya kecil banget dibanding biaya bersihin ribuan data yatim tiga bulan kemudian.
Primary key nandain tiap baris di tabelnya sendiri secara unik. Nilainya gak boleh kembar dan gak boleh kosong. Foreign key ada di tabel lain dan nunjuk ke primary key itu. Satu tabel cuma boleh punya satu primary key, tapi bisa punya banyak foreign key.
Secara teknis gak wajib, database tetap mau bikin tabelnya. Tapi tanpa primary key kamu gak bisa nunjuk satu baris tertentu dengan pasti, dan tabel lain gak bisa nyambung ke situ. Baris duplikat juga bisa masuk tanpa ketauan. Kasih primary key ke setiap tabel, sekalipun cuma kolom id yang naik otomatis.
Bisa, kecuali kamu kasih NOT NULL. Foreign key kosong artinya baris itu belum punya induk, misalnya transaksi dari pembeli yang gak daftar akun. Kalau tiap transaksi wajib punya pelanggan, tambahin NOT NULL supaya database nolak baris tanpa induk.
Primary key yang terdiri dari dua kolom atau lebih. Dipakai kalau gak ada satu kolom pun yang unik sendirian. Contohnya tabel detail_transaksi: id_transaksi bisa muncul berkali-kali, id_produk juga, tapi kombinasi keduanya unik.
Kalau baris induk dihapus, semua baris anak yang nunjuk ke situ ikut kehapus otomatis. Ini praktis tapi bahaya buat data keuangan. Buat tabel transaksi, pakai ON DELETE RESTRICT. Aturan lengkapnya bisa kamu baca di dokumentasi constraint PostgreSQL.
Tiga hal yang perlu kamu bawa:
Coba jalanin query cek data yatim di database yang kamu pegang sekarang. Kalau ada yang keluar, kamu baru aja nemu bocor yang selama ini gak keliatan di laporan.
Lanjut ngulik relasi antar tabel? Baca panduan JOIN di SQL dari nol. Buat yang mau ngerti aturan lain yang jaga kualitas data, ada juga penjelasan constraint SQL lengkap.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Analisa per kuartal, hari kerja, atau musim jadi ribet kalau tiap query ngitung ulang atribut tanggal. Tabel kalender nyimpen semua atribut itu sekali, biar tinggal di-JOIN.
Laporan penjualan harian sering bolong di hari tanpa transaksi. Ini cara bikin deret tanggal lengkap di SQL biar tiap hari muncul, walau nilainya nol.
Struktur organisasi tersimpan sebagai kolom id_atasan yang saling nunjuk. Ini cara narik rantai jabatan, hitung total bawahan, dan span of control cuma pakai SQL.