Sejauh ini semua latihan kita memakai satu tabel yang berdiri sendiri. Aplikasi nyata hampir tidak pernah sesederhana itu: data pelanggan, pesanan, produk, dan pembayaran disimpan di tabel-tabel terpisah yang saling terhubung. Pemisahan tersebut bukan formalitas, melainkan inti dari database relasional — dan JOIN, topik utama section ini, adalah alat untuk menggabungkan kembali data yang sengaja dipecah itu. Sebelum menyentuh sintaks JOIN, kita perlu memahami dulu tiga hal: mengapa data dipecah, bagaimana dua tabel dihubungkan lewat kolom kunci, dan bagaimana proses penggabungannya bekerja secara manual. Chapter ini membahas ketiganya memakai sepasang tabel yang akan menemani kita di seluruh section: pelanggan dan pesanan.

Redundansi: Biaya Menyimpan Segalanya di Satu Tabel #

Cara paling naif mendesain data pesanan adalah menaruh semuanya di satu tabel lebar: identitas pelanggan ditulis ulang pada setiap baris pesanan. Kita buat tabel pesanan_flat semacam itu untuk melihat masalahnya secara nyata:

sql
DROP DATABASE IF EXISTS belajar_join;
CREATE DATABASE belajar_join;
USE belajar_join;

CREATE TABLE pesanan_flat (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nama VARCHAR(50) NOT NULL,
  kota VARCHAR(30) NOT NULL,
  tanggal DATE NOT NULL,
  total DECIMAL(12,2) NOT NULL
);

INSERT INTO pesanan_flat (nama, kota, tanggal, total) VALUES
  ('Siti Rahayu', 'Bandung', '2026-01-07',  50000.00),
  ('Siti Rahayu', 'Bandung', '2026-01-18',  85000.00),
  ('Andi Wijaya', 'Surabaya', '2026-01-15', 137000.00);

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, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0
sql
SELECT * FROM pesanan_flat ORDER BY id;

Output yang Dihasilkan:

text
+----+-------------+----------+------------+-----------+
| id | nama        | kota     | tanggal    | total     |
+----+-------------+----------+------------+-----------+
|  1 | Siti Rahayu | Bandung  | 2026-01-07 |  50000.00 |
|  2 | Siti Rahayu | Bandung  | 2026-01-18 |  85000.00 |
|  3 | Andi Wijaya | Surabaya | 2026-01-15 | 137000.00 |
+----+-------------+----------+------------+-----------+
3 rows in set (0.00 sec)

Dua baris pertama memuat informasi yang persis sama tentang Siti: namanya dan kotanya ditulis dua kali. Pengulangan semacam ini disebut redundansi, dan redundansi bukan sekadar borong ruang simpan. Bayangkan Siti pindah dari Bandung ke Jakarta: kita harus mengubah banyak baris sekaligus, dan satu baris yang terlewat membuat data tidak konsisten — sebagian baris mengatakan Bandung, sebagian Jakarta. Masalah ketika pembaruan data menjadi tidak aman semacam ini dikenal sebagai anomaly pembaruan, dan inilah alasan utama proses normalisasi memecah data ke tabel-tabel kecil: identitas pelanggan cukup disimpan satu kali, lalu tabel lain cukup menyimpan rujukan padanya.

Relasi 1-N Lewat Foreign Key #

Desain yang benar memecah data tadi menjadi dua tabel. Tabel pelanggan menyimpan identitas sekali saja; tabel pesanan menyimpan transaksi dan hanya mencatat siapa pelanggannya lewat kolom pelanggan_id:

sql
CREATE TABLE pelanggan (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nama VARCHAR(50) NOT NULL,
  kota VARCHAR(30) NOT NULL
);

CREATE TABLE pesanan (
  id INT AUTO_INCREMENT PRIMARY KEY,
  pelanggan_id INT NOT NULL,
  tanggal DATE NOT NULL,
  total DECIMAL(12,2) NOT NULL,
  FOREIGN KEY (pelanggan_id) REFERENCES pelanggan(id)
);

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Satu orang pelanggan bisa memiliki banyak pesanan, tetapi satu pesanan hanya dimiliki oleh satu pelanggan. Hubungan seperti ini disebut relasi satu-ke-banyak (1-N), dan arahnya dibaca dari tabel induk ke tabel anak: pelanggan adalah sisi “satu”, pesanan adalah sisi “banyak”. Kolom pelanggan_id pada tabel anak berperan sebagai foreign key — nilainya wajib meniru nilai id milik satu baris di tabel induk. Klausa FOREIGN KEY (pelanggan_id) REFERENCES pelanggan(id) mengaktifkan penegakan hubungan ini di level server, sehingga baris pesanan tidak bisa menunjuk pelanggan yang tidak ada:

sql
-- @error
INSERT INTO pesanan (pelanggan_id, tanggal, total) VALUES (99, '2026-01-25', 50000.00);

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`belajar_join`.`pesanan`, CONSTRAINT `pesanan_ibfk_1` FOREIGN KEY (`pelanggan_id`) REFERENCES `pelanggan` (`id`))
Bye

Server menolak penyisipan tersebut karena tidak ada pelanggan bernomor 99 — relasi antartabel dijaga konsisten oleh constraint, bukan oleh kedisiplinan kita menulis data. Diagram berikut memetakan hubungan kedua tabel dalam notasi entity-relationship, dengan simbol ||--o{ yang berarti “satu baris pelanggan terhubung nol atau banyak baris pesanan”:

Kolom Kunci sebagai Jembatan Data Terpisah #

Kedua tabel kini berisi data yang lengkap. Kita isi datanya dengan pola yang dipakai di seluruh section ini — perhatikan bahwa beberapa pelanggan sengaja tidak diberi pesanan, dan satu pelanggan diberi tiga pesanan sekaligus:

sql
INSERT INTO pelanggan (nama, kota) VALUES
  ('Budi Santoso', 'Jakarta'),
  ('Siti Rahayu',  'Bandung'),
  ('Andi Wijaya',  'Surabaya'),
  ('Rina Melati',  'Yogyakarta'),
  ('Dewi Lestari', 'Bandung'),
  ('Tono Hartono', 'Medan'),
  ('Sari Puspita', 'Jakarta');

INSERT INTO pesanan (pelanggan_id, tanggal, total) VALUES
  (1, '2026-01-05', 170000.00),
  (2, '2026-01-07',  50000.00),
  (1, '2026-01-10',  62000.00),
  (1, '2026-01-12',  15000.00),
  (3, '2026-01-15', 137000.00),
  (2, '2026-01-18',  85000.00),
  (4, '2026-01-08', 125000.00);

Output yang Dihasilkan:

text
Query OK, 7 rows affected (0.00 sec)
Records: 7  Duplicates: 0  Warnings: 0

Query OK, 7 rows affected (0.00 sec)
Records: 7  Duplicates: 0  Warnings: 0
sql
SELECT * FROM pelanggan ORDER BY id;

Output yang Dihasilkan:

text
+----+--------------+------------+
| id | nama         | kota       |
+----+--------------+------------+
|  1 | Budi Santoso | Jakarta    |
|  2 | Siti Rahayu  | Bandung    |
|  3 | Andi Wijaya  | Surabaya   |
|  4 | Rina Melati  | Yogyakarta |
|  5 | Dewi Lestari | Bandung    |
|  6 | Tono Hartono | Medan      |
|  7 | Sari Puspita | Jakarta    |
+----+--------------+------------+
7 rows in set (0.00 sec)
sql
SELECT * FROM pesanan ORDER BY id;

Output yang Dihasilkan:

text
+----+--------------+------------+-----------+
| id | pelanggan_id | tanggal    | total     |
+----+--------------+------------+-----------+
|  2 |            1 | 2026-01-05 | 170000.00 |
|  3 |            2 | 2026-01-07 |  50000.00 |
|  4 |            1 | 2026-01-10 |  62000.00 |
|  5 |            1 | 2026-01-12 |  15000.00 |
|  6 |            3 | 2026-01-15 | 137000.00 |
|  7 |            2 | 2026-01-18 |  85000.00 |
|  8 |            4 | 2026-01-08 | 125000.00 |
+----+--------------+------------+-----------+
7 rows in set (0.00 sec)

Data pesanan memang tidak lagi menyimpan nama pelanggan — dan justru di situlah letak desainnya. Nilai pelanggan_id pada setiap baris pesanan adalah alamat menuju baris pelanggan yang dimaksud: pesanan nomor satu sampai tiga menunjuk pelanggan_id bernilai 1, 2, dan kembali 1. Selama dua tabel berbagi kolom kunci yang nilainya saling terhubung seperti ini, data yang terpisah selalu bisa digabungkan kembali. Kolom kunci inilah jembatan yang akan dipakai setiap jenis JOIN di chapter-chapter berikutnya.

Menggabungkan Secara Manual: Query Dua Langkah #

Sebelum ada JOIN, kita sebenarnya sudah sanggup menggabungkan kedua tabel secara manual dengan dua query berurutan. Misalkan kita ingin tahu semua pesanan milik Siti. Langkah pertama, cari nomor identitas Siti di tabel pelanggan:

sql
SELECT id, nama FROM pelanggan WHERE nama = 'Siti Rahayu';

Output yang Dihasilkan:

text
+----+-------------+
| id | nama        |
+----+-------------+
|  2 | Siti Rahayu |
+----+-------------+
1 row in set (0.00 sec)

Siti ternyata menyimpan nomor identitas 2. Langkah kedua, pakai nomor tersebut sebagai filter di tabel pesanan:

sql
SELECT id, tanggal, total FROM pesanan WHERE pelanggan_id = 2 ORDER BY id;

Output yang Dihasilkan:

text
+----+------------+----------+
| id | tanggal    | total    |
+----+------------+----------+
|  3 | 2026-01-07 | 50000.00 |
|  7 | 2026-01-18 | 85000.00 |
+----+------------+----------+
2 rows in set (0.00 sec)

Dua query itu bekerja, tetapi prosesnya merepotkan: kita harus membaca hasil query pertama dengan mata, menyalin angkanya, lalu menyusun query kedua secara manual. Bayangkan laporan bulanan yang butuh nama pelanggan untuk seluruh ribuan pesanan — pendekatan dua langkah ini tidak bisa diotomatisasi dalam satu query tunggal. JOIN menyelesaikan persis masalah ini: satu query yang membaca kedua tabel sekaligus, mencocokkan pelanggan.id dengan pesanan.pelanggan_id secara otomatis, dan menampilkan hasilnya sebagai satu tabel gabungan. Kita akan menulis JOIN pertama kita di chapter berikutnya.

Membersihkan Database Latihan #

sql
DROP DATABASE belajar_join;

Output yang Dihasilkan:

text
Query OK, 3 rows affected (0.00 sec)

Referensi #

Data yang dipecah ke tabel terpisah tersimpan tanpa redundansi dan dijaga konsisten oleh foreign key. Dua tabel terpisah di atas dapat digabungkan kembali karena berbagi kolom kunci id dan pelanggan_id yang saling terhubung. Chapter berikutnya memperkenalkan INNER JOIN — cara pertama dan paling sering dipakai untuk menggabungkan kedua tabel tersebut dalam satu query tunggal.