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:

AlasanContoh kolomManfaat pemisahan
kolom jarang dipakaibio, tanggal_lahirtabel utama ramping; query yang tak butuh profil tak memuat kolom pelengkap
data sensitifkolom keuangan atau NIKtabel terpisah lebih mudah dibatasi hak aksesnya
kolom besarfoto (BLOB), dokumentabel 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_id sama — otomatis memastikan maksimal satu profil per pengguna;
  • foreign key menolak profil yang menunjuk pengguna yang tidak ada;
  • kolom penghubung tidak perlu AUTO_INCREMENT tambahan 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:

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

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

  1. id INT AUTO_INCREMENT PRIMARY KEY pada pengguna — identitas buatan yang bertambah otomatis, pola standar entitas kuat.
  2. username ... UNIQUE dan email ... UNIQUE — keduanya juga identitas alami; MySQL menolak pendaftaran akun dengan email yang sudah dipakai.
  3. user_id INT PRIMARY KEY pada profil_detail — kolom penghubung langsung dijadikan primary key, tanpa AUTO_INCREMENT, sehingga satu pengguna tidak mungkin punya dua baris profil.
  4. CONSTRAINT fk_profil_user FOREIGN KEY (user_id) REFERENCES pengguna(id) — constraint bernama eksplisit membuat pesan galat mudah dikenali; baris profil hanya boleh menunjuk id yang benar-benar ada di pengguna.
  5. NULL eksplisit pada bio, 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:

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

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

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

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

text
+----------+--------------+---------+
| username | nama_lengkap | kota    |
+----------+--------------+---------+
| budi     | Budi Santoso | Jakarta |
| siti     | Siti Rahayu  | Bandung |
+----------+--------------+---------+
2 rows in set (0.00 sec)
sql
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:

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

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

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

sql
-- @error
INSERT INTO pengaturan (user_id, tema, bahasa) VALUES (1, 'terang', 'en');

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1062 (23000): Duplicate entry '1' for key 'pengaturan.uq_pengaturan_user'
Bye

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

sql
DROP DATABASE belajar_rdb;

Output yang Dihasilkan:

text
Query OK, 3 rows affected (0.00 sec)

Referensi #

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.