Bentuk hubungan paling sederhana antardua tabel adalah relasi satu-ke-satu (one-to-one): satu baris pada tabel pertama berpasangan paling banyak dengan satu baris pada tabel kedua, dan sebaliknya. Kalau dua tabel saling mencocokkan satu-satu, kenapa tidak digabung saja menjadi satu tabel? Pertanyaan itu justru inti chapter ini — pemisahan satu-ke-satu hampir selalu lahir dari keputusan desain, bukan dari sifat datanya. Kita akan memakai contoh pengguna dan profil_detail dari aplikasi dengan akun user, membangun dua pola kunci yang menegakkan relasi ini, lalu membuktikan relasinya lewat JOIN.
Konsep Relasi Satu-ke-Satu #
Relasi satu-ke-satu menyatakan bahwa setiap baris entitas A memiliki paling banyak satu baris entitas B. Pada aplikasi dengan akun pengguna, tabel pengguna menyimpan data inti yang hampir semua query butuhkan — username dan email — sedangkan tabel profil_detail menyimpan data pelengkap yang jarang dibaca. Satu pengguna punya paling banyak satu profil, dan satu profil milik tepat satu pengguna. Tiga alasan klasis mendorong pemisahan seperti ini:
| Alasan | Contoh kolom | Manfaat pemisahan |
|---|---|---|
| kolom jarang dipakai | bio, tanggal_lahir | tabel utama ramping; query yang tak butuh profil tak memuat kolom pelengkap |
| data sensitif | kolom keuangan atau NIK | tabel terpisah lebih mudah dibatasi hak aksesnya |
| kolom besar | foto (BLOB), dokumen | tabel utama tetap ringan saat dipindai |
Alasan pertama bekerja seperti rak ganda di toko: barang yang sering diambil ditaruh di rak depan, barang musiman disimpan di gudang. Query login misalnya hanya membaca username dan email ribuan kali per detik; kolom bio yang panjang tidak ikut terangkut pada pemindaian tabel pengguna karena tinggal di tabel lain. Alasan kedua dan ketiga mengikuti logika yang sama: memisahkan data berdasarkan pola pemakaian, sensitivitas, dan ukurannya.
Pola Kunci: Primary Key Sekaligus Foreign Key #
Bagaimana MySQL menegakkan “paling banyak satu profil per pengguna”? Kuncinya ada pada kolom penghubung di sisi profil. Pola paling bersih menjadikan user_id sebagai primary key tabel profil_detail sekaligus foreign key menuju pengguna.id:
- primary key menolak dua baris profil dengan
user_idsama — otomatis memastikan maksimal satu profil per pengguna; - foreign key menolak profil yang menunjuk pengguna yang tidak ada;
- kolom penghubung tidak perlu
AUTO_INCREMENTtambahan karena identitas profil memang berasal dari pemiliknya.
Praktik: Membuat Tabel, Mengisi Data, dan Menggabungkan #
Kita bangun kedua tabel di database latihan belajar_rdb. Perhatikan deklarasi user_id yang menjadi primary key sekaligus foreign key:
DROP DATABASE IF EXISTS belajar_rdb;
CREATE DATABASE belajar_rdb;
USE belajar_rdb;
CREATE TABLE pengguna (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(30) NOT NULL UNIQUE,
email VARCHAR(80) NOT NULL UNIQUE
);
CREATE TABLE profil_detail (
user_id INT PRIMARY KEY,
nama_lengkap VARCHAR(60) NOT NULL,
bio VARCHAR(200) NULL,
tanggal_lahir DATE NULL,
kota VARCHAR(30) NULL,
CONSTRAINT fk_profil_user
FOREIGN KEY (user_id) REFERENCES pengguna(id)
);Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 1 row affected (0.00 sec)
Database changed
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)Mari kita bedah bagian-bagian pentingnya:
id INT AUTO_INCREMENT PRIMARY KEYpadapengguna— identitas buatan yang bertambah otomatis, pola standar entitas kuat.username ... UNIQUEdanemail ... UNIQUE— keduanya juga identitas alami; MySQL menolak pendaftaran akun dengan email yang sudah dipakai.user_id INT PRIMARY KEYpadaprofil_detail— kolom penghubung langsung dijadikan primary key, tanpaAUTO_INCREMENT, sehingga satu pengguna tidak mungkin punya dua baris profil.CONSTRAINT fk_profil_user FOREIGN KEY (user_id) REFERENCES pengguna(id)— constraint bernama eksplisit membuat pesan galat mudah dikenali; baris profil hanya boleh menunjukidyang benar-benar ada dipengguna.NULLeksplisit padabio,tanggal_lahir,kota— data pelengkap memang boleh kosong; profil bisa dibuat dulu, diisi belakangan.
Selanjutnya kita isi tiga pengguna tetapi hanya dua profil — satu pengguna sengaja dibiarkan tanpa profil untuk melihat perilaku JOIN:
INSERT INTO pengguna (username, email) VALUES
('budi', '[email protected]'),
('siti', '[email protected]'),
('andi', '[email protected]');
INSERT INTO profil_detail (user_id, nama_lengkap, bio, tanggal_lahir, kota) VALUES
(1, 'Budi Santoso', 'Suka memperbaiki peralatan rumah tangga.', '1995-03-12', 'Jakarta'),
(2, 'Siti Rahayu', 'Penggemar tanaman hias.', '1998-07-21', 'Bandung');Output yang Dihasilkan:
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0Perhatikan urutan pengerjaannya: pengguna harus terisi lebih dulu karena foreign key di profil_detail akan memeriksa setiap user_id yang kita masukkan. Nilai 1 dan 2 pada kolom user_id merujuk nomor baris pengguna yang baru dibuat — pada tabel kosong dengan AUTO_INCREMENT, urutan INSERT menentukan nomor tersebut secara pasti. Sekarang kita gabungkan kedua tabel:
SELECT p.username, pd.nama_lengkap, pd.kota
FROM pengguna p
INNER JOIN profil_detail pd ON p.id = pd.user_id
ORDER BY p.id;Output yang Dihasilkan:
+----------+--------------+---------+
| username | nama_lengkap | kota |
+----------+--------------+---------+
| budi | Budi Santoso | Jakarta |
| siti | Siti Rahayu | Bandung |
+----------+--------------+---------+
2 rows in set (0.00 sec)SELECT p.username, pd.nama_lengkap, pd.kota
FROM pengguna p
LEFT JOIN profil_detail pd ON p.id = pd.user_id
ORDER BY p.id;Output yang Dihasilkan:
+----------+--------------+---------+
| username | nama_lengkap | kota |
+----------+--------------+---------+
| budi | Budi Santoso | Jakarta |
| siti | Siti Rahayu | Bandung |
| andi | NULL | NULL |
+----------+--------------+---------+
3 rows in set (0.00 sec)INNER JOIN hanya menghasilkan dua baris karena hanya dua pengguna memiliki profil; andi tidak punya pasangan sehingga gugur. LEFT JOIN mempertahankan seluruh sisi kiri, dan kekosongan pasangan andi terisi NULL — perilaku persis seperti yang sudah kita kenal di section joins. Dari sisi desain, hasil LEFT JOIN inilah yang mengonfirmasi bentuk relasinya: tiga pengguna, dua profil, tanpa satu pun pengguna yang muncul dua kali.
Diagram entity-relationship berikut merangkum kedua pola kunci — pengguna menjadi pusat yang dihubungi profil_detail dengan kunci digabung, dan pengaturan dengan kunci terpisah plus UNIQUE:
Alternatif: Foreign Key dengan Constraint UNIQUE #
Pola pertama menjadikan user_id sebagai primary key tabel profil. Kadang tabel sisi kedua tetap butuh primary key sendiri — misalnya karena barisnya juga dirujuk tabel lain — sehingga pemetaannya dibalik: kolom penghubung menjadi foreign key biasa yang dilindungi constraint UNIQUE. Kita buat tabel pengaturan (satu baris preferensi per pengguna) dengan pola kedua:
CREATE TABLE pengaturan (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
tema VARCHAR(20) NOT NULL DEFAULT 'terang',
bahasa VARCHAR(10) NOT NULL DEFAULT 'id',
CONSTRAINT uq_pengaturan_user UNIQUE (user_id),
CONSTRAINT fk_pengaturan_user
FOREIGN KEY (user_id) REFERENCES pengguna(id)
);
INSERT INTO pengaturan (user_id, tema, bahasa) VALUES
(1, 'gelap', 'id');Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 1 row affected (0.00 sec)Constraint UNIQUE pada user_id kini yang menjaga kardinalitas satu-ke-satu. Bukti penegakannya langsung terlihat ketika kita mencoba menambah baris kedua untuk pengguna yang sama:
-- @error
INSERT INTO pengaturan (user_id, tema, bahasa) VALUES (1, 'terang', 'en');Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1062 (23000): Duplicate entry '1' for key 'pengaturan.uq_pengaturan_user'
ByeServer menolak penyisipan tersebut dengan galat Duplicate entry karena nilai user_id = 1 sudah tercatat di tabel pengaturan. Dua pola tersebut setara secara hasil — pilih pola PK-FK gabungan ketika tabel kedua murni pelengkap, dan pola UNIQUE ketika tabel kedua punya identitas sendiri.
Kapan Memecah, Kapan Tidak #
Pemisahan satu-ke-satu membawa biaya: setiap pembacaan data lengkap kini memerlukan JOIN, dan penulisan data tersebar ke dua tabel. Untuk tiga sampai lima kolom kecil yang hampir selalu ikut dibaca — misalnya nama_lengkap pada profil — penggabungan ke tabel utama justru lebih sederhana. Pemisahan menjadi masuk akal ketika salah satu alasan pada tabel di awal chapter terasa nyata: kolomnya besar, sensitif, atau benar-benar jarang disentuh. Aturan praktisnya: mulailah dari satu tabel, pecahkan saat pola pemakaian data terbukti berbeda.
Membersihkan Database Latihan #
DROP DATABASE belajar_rdb;Output yang Dihasilkan:
Query OK, 3 rows affected (0.00 sec)Referensi #
- FOREIGN KEY Constraints — MySQL 8.4 Reference Manual
- CREATE TABLE Statement — MySQL 8.4 Reference Manual
- JOIN Clause — MySQL 8.4 Reference Manual
Relasi satu-ke-satu kini tuntas: kita tahu kapan memecah tabel, dan dua pola kunci yang menegakkan batas “paling banyak satu” di sisi MySQL. Namun hubungan yang paling sering muncul di aplikasi bukanlah ini — satu pelanggan membuat banyak pesanan, satu proyek memuat banyak tugas. Chapter berikutnya membahas relasi satu-ke-banyak beserta aturan penempatan foreign key yang membuatnya bekerja.