Primary key menjaga identitas baris, tetapi identitas bukan satu-satunya data yang tidak boleh kembar: satu alamat email hanya boleh dimiliki satu akun, dan satu kombinasi kota-nama hanya boleh dipakai satu cabang toko. Kebutuhan seperti itu tidak pantas memakai primary key — email bukan identitas teknis tabel — dan di sinilah UNIQUE berperan: constraint yang menolak nilai duplikat pada satu kolom atau gabungan kolom, dengan syarat lebih longgar daripada primary key. Pada chapter ini kita akan menguji UNIQUE pada kolom email dan pasangan kolom kota-nama, menyaksikan galat 1062 saat duplikat masuk, dan membuktikan satu kejutan klasik: kolom UNIQUE ternyata masih menerima banyak baris NULL.
Menjaga Keunikan Satu Kolom #
Kita siapkan database latihan dan tabel pengguna untuk aplikasi dengan akun. Kolom email wajib unik — dua akun tidak boleh memakai email yang sama — sedangkan kolom telepon kita jadikan contoh kolom unik yang opsional:
CREATE DATABASE belajar_unik;
USE belajar_unik;Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)
Database changedCREATE TABLE pengguna (
id INT PRIMARY KEY,
email VARCHAR(100) NOT NULL UNIQUE,
nama VARCHAR(100) NOT NULL,
telepon VARCHAR(20) UNIQUE
);Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)Mari kita bedah baris-baris yang membawa constraint:
- Baris 2 (
id INT PRIMARY KEY): Identitas teknis tetap dipegang kolomid— pola kunci sintetis dari chapter sebelumnya. - Baris 3 (
email VARCHAR(100) NOT NULL UNIQUE): Kata kunciUNIQUEmenjadikan nilai email unik di seluruh tabel. KombinasiNOT NULL UNIQUEberarti email wajib diisi dan tidak boleh kembar. - Baris 5 (
telepon VARCHAR(20) UNIQUE): TanpaNOT NULL, kolom ini unik hanya untuk nilai yang terisi — aturan khusus untukNULLakan kita buktikan sebentar lagi.
Klausa UNIQUE otomatis membangun index di belakang layar, sehingga pengecekan duplikat dilakukan dengan cepat meski tabel berisi jutaan baris. Kita isi dua akun pertama:
INSERT INTO pengguna (id, email, nama, telepon) VALUES
(1, '[email protected]', 'Budi', '081211112222'),
(2, '[email protected]', 'Siti', NULL);
SELECT id, email, nama, telepon FROM pengguna ORDER BY id;Output yang Dihasilkan:
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0
+----+------------------+------+--------------+
| id | email | nama | telepon |
+----+------------------+------+--------------+
| 1 | [email protected] | Budi | 081211112222 |
| 2 | [email protected] | Siti | NULL |
+----+------------------+------+--------------+
2 rows in set (0.00 sec)Duplikat Ditolak dengan Galat 1062 #
Sekarang akun ketiga mencoba mendaftar dengan email yang sudah dipakai Budi:
-- @error
INSERT INTO pengguna (id, email, nama, telepon) VALUES (3, '[email protected]', 'Ani', NULL);Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1062 (23000): Duplicate entry '[email protected]' for key 'pengguna.email'
ByeServer menolak pendaftaran itu dengan galat ERROR 1062 (23000): Duplicate entry '[email protected]' for key 'pengguna.email' — pesan yang sama dengan pelanggaran primary key, hanya nama penjaganya kini pengguna.email, mengikuti index unik kolom email. Baris Ani tidak tersimpan sama sekali, sehingga pemilik akun asli tetap satu.
Kejutan NULL yang Boleh Ganda #
Inilah perilaku yang paling sering mengejutkan pemula: kolom UNIQUE masih menerima banyak baris NULL. Nilai NULL berarti “tidak diketahui”, dan dua nilai yang tidak diketahui tidak dianggap sama satu sama lain. Kita buktikan dengan dua pengguna baru yang sama-sama belum mencantumkan telepon:
INSERT INTO pengguna (id, email, nama, telepon) VALUES
(3, '[email protected]', 'Ani', NULL),
(4, '[email protected]', 'Joko', NULL);
SELECT id, email, nama, telepon FROM pengguna ORDER BY id;Output yang Dihasilkan:
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0
+----+------------------+------+--------------+
| id | email | nama | telepon |
+----+------------------+------+--------------+
| 1 | [email protected] | Budi | 081211112222 |
| 2 | [email protected] | Siti | NULL |
| 3 | [email protected] | Ani | NULL |
| 4 | [email protected] | Joko | NULL |
+----+------------------+------+--------------+
4 rows in set (0.00 sec)Kedua baris diterima — kolom telepon kini memuat tiga nilai NULL (Siti, Ani, dan Joko) tanpa pelanggaran constraint. Bandingkan dengan kolom email: karena NOT NULL UNIQUE, kombinasi keduanya menolak kembar sekaligus menolak kosong. Pola NOT NULL UNIQUE dan UNIQUE saja dengan demikian menjawab dua kebutuhan berbeda.
UNIQUE Multi Kolom #
Keunikan sering hanya berlaku untuk kombinasi, bukan tiap kolom sendiri-sendiri: cabang toko boleh bernama Pusat di banyak kota, tetapi satu kota hanya boleh punya satu Pusat. Aturan seperti itu dinyatakan sebagai constraint tingkat tabel:
CREATE TABLE cabang (
id INT PRIMARY KEY,
kota VARCHAR(50) NOT NULL,
nama VARCHAR(50) NOT NULL,
UNIQUE kota_nama (kota, nama)
);Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)Bedahnya singkat namun penting:
- Baris 4 (
UNIQUE kota_nama (kota, nama)): Constraint diberi namakota_nama, lalu tanda kurung mencantumkan kolom yang gabungannya harus unik. Nilaikotaboleh kembar dan nilainamaboleh kembar — yang dilarang hanya pasangan yang persis sama. - Nama constraint: Nama
kota_namaakan muncul di pesan galat, jadi beri nama yang deskriptif sejak awal.
Kita isi tiga cabang yang aman — perhatikan dua baris pertama sama-sama bernama Pusat di kota berbeda:
INSERT INTO cabang (id, kota, nama) VALUES
(1, 'Jakarta', 'Pusat'),
(2, 'Bandung', 'Pusat'),
(3, 'Surabaya', 'Timur');
SELECT id, kota, nama FROM cabang ORDER BY id;Output yang Dihasilkan:
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0
+----+----------+-------+
| id | kota | nama |
+----+----------+-------+
| 1 | Jakarta | Pusat |
| 2 | Bandung | Pusat |
| 3 | Surabaya | Timur |
+----+----------+-------+
3 rows in set (0.00 sec)Duplikat kombinasi akan ditolak:
-- @error
INSERT INTO cabang (id, kota, nama) VALUES (4, 'Jakarta', 'Pusat');Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1062 (23000): Duplicate entry 'Jakarta-Pusat' for key 'cabang.kota_nama'
ByeGalat 1062 kembali muncul, dan nilainya ditampilkan sebagai pasangan: Duplicate entry 'Jakarta-Pusat' for key 'cabang.kota_nama' — format nilai1-nilai2 mencerminkan gabungan dua kolom yang dikunci. Sebaliknya, pasangan yang sebagian saja sama tetap sah:
INSERT INTO cabang (id, kota, nama) VALUES (4, 'Jakarta', 'Selatan');
SELECT id, kota, nama FROM cabang ORDER BY id;Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)
+----+----------+---------+
| id | kota | nama |
+----+----------+---------+
| 1 | Jakarta | Pusat |
| 2 | Bandung | Pusat |
| 3 | Surabaya | Timur |
| 4 | Jakarta | Selatan |
+----+----------+---------+
4 rows in set (0.00 sec)Baris keempat diterima karena Jakarta-Selatan belum pernah ada, walau kotanya sama dengan baris pertama. Prinsip “keunikan berlaku pada gabungan” ini identik dengan cara kerja composite primary key yang akan kita bahas di chapter khusus.
UNIQUE vs PRIMARY KEY #
Kedua constraint sama-sama menjaga keunikan dan sama-sama diimplementasikan lewat index, sehingga membingungkan bila disandingkan. Perbandingan berikut memisahkan perannya:
| Aspek | PRIMARY KEY | UNIQUE |
|---|---|---|
| Jumlah per tabel | maksimal satu | boleh banyak |
Nilai NULL | dilarang | diperbolehkan (banyak baris) |
| Posisi di desain | identitas resmi baris | aturan bisnis tambahan |
| Sifat index | clustered pada InnoDB | index biasa (secondary) |
| Penamaan | selalu bernama PRIMARY | dinamai atau otomatis |
Kesimpulan praktisnya: setiap tabel punya satu primary key sebagai identitas, lalu UNIQUE dipasang pada kolom-kolom bisnis yang nilainya tidak boleh kembar — email akun, kode SKU barang, atau kombinasi kota dan nama cabang.
Referensi #
Keunikan kolom email dan pasangan kota-nama kini dijaga server secara otomatis. Namun constraint UNIQUE dan PRIMARY KEY sama-sama menolak nilai yang sudah ada — bagaimana dengan kolom yang nilainya memang belum tersedia? Perbedaan antara “belum ada nilainya” dan “nilainya kosong” dibedah tuntas oleh chapter NOT NULL Constraint. Sebelum itu, kita bereskan database latihan:
DROP DATABASE IF EXISTS belajar_unik;Output yang Dihasilkan:
Query OK, 2 rows affected (0.00 sec)Dua tabel — pengguna dan cabang — ikut terbuang bersama constraint uniknya.