Desain database yang baik memecah data ke dalam tabel-tabel kecil: pelanggan, produk, pesanan, detail. Konsekuensinya, jawaban atas pertanyaan bisnis hampir selalu tersebar di beberapa tabel sekaligus — “siapa yang membeli apa” tidak mungkin dijawab dari satu tabel saja. Operator JOIN menyatukan kembali potongan-potongan itu dengan mencocokkan baris dari satu tabel ke baris tabel lain lewat kolom penghubung. Kita akan membangun dari INNER JOIN paling dasar hingga LEFT JOIN, self-join, dan CROSS JOIN.
Data Contoh #
Empat tabel domain toko dipakai sepanjang chapter ini:
CREATE TABLE pelanggan (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL,
kota TEXT
);
CREATE TABLE produk (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL,
harga INTEGER NOT NULL,
stok INTEGER
);
CREATE TABLE pesanan (
id INTEGER PRIMARY KEY,
pelanggan_id INTEGER REFERENCES pelanggan(id),
tanggal TEXT NOT NULL
);
CREATE TABLE detail_pesanan (
pesanan_id INTEGER REFERENCES pesanan(id),
produk_id INTEGER REFERENCES produk(id),
jumlah INTEGER NOT NULL,
PRIMARY KEY (pesanan_id, produk_id)
);
INSERT INTO pelanggan (id, nama, kota) VALUES
(1, 'Budi', 'Jakarta'), (2, 'Siti', 'Bandung'), (3, 'Ani', NULL),
(4, 'Rina', 'Surabaya'), (5, 'Dewi', 'Jakarta');
INSERT INTO produk (id, nama, harga, stok) VALUES
(1, 'Kopi Gayo', 45000, 20), (2, 'Teh Melati', 18000, 35),
(3, 'Gula Aren', 25000, 15), (4, 'Sirup Vanila', 25000, NULL),
(5, 'Kopi Toraja', 52000, 10), (6, 'Teh Hijau', 22000, 0),
(7, 'Cangkir Keramik', 35000, 8);
INSERT INTO pesanan (id, pelanggan_id, tanggal) VALUES
(1, 1, '2025-06-01'), (2, 2, '2025-06-03'), (3, 1, '2025-06-07'),
(4, 3, '2025-06-10'), (5, 2, '2025-06-15'), (6, 1, '2025-07-02');
INSERT INTO detail_pesanan (pesanan_id, produk_id, jumlah) VALUES
(1, 1, 2), (1, 3, 1), (2, 2, 3), (3, 1, 1),
(3, 4, 2), (4, 5, 1), (5, 6, 2), (5, 3, 2), (6, 1, 1);INNER JOIN: Hanya Baris yang Cocok #
INNER JOIN memasangkan setiap baris pelanggan dengan baris pesanan yang pelanggan_id-nya sama. Klausa ON menentukan syarat pasangannya, dan alias singkat (p, o) membuat query ringkas:
SELECT p.nama AS pelanggan, o.id AS pesanan, o.tanggal
FROM pelanggan p
INNER JOIN pesanan o ON o.pelanggan_id = p.id
ORDER BY o.id;Output yang Dihasilkan:
Budi|1|2025-06-01
Siti|2|2025-06-03
Budi|3|2025-06-07
Ani|4|2025-06-10
Siti|5|2025-06-15
Budi|6|2025-07-02Mari kita bedah query di atas:
- Baris 1 (
SELECT p.nama AS pelanggan, o.id AS pesanan, o.tanggal) — memilih kolom dari dua tabel sekaligus; prefiksp.dano.menandakan tabel asal tiap kolom, danASmemberi nama kolom hasil. - Baris 2 (
FROM pelanggan p) — tabel pertama, dengan aliasp. - Baris 3 (
INNER JOIN pesanan o ON o.pelanggan_id = p.id) — tabel kedua dipasangkan dengan syaratpelanggan_iddipesanansama denganiddipelanggan.
Enam pesanan tampil dengan nama pembelinya. Rina dan Dewi tidak muncul — keduanya tidak punya pesanan, dan INNER JOIN memang hanya mempertahankan pasangan yang cocok.
Merantai Beberapa JOIN #
Alur lengkap “siapa membeli apa” membutuhkan empat tabel sekaligus: pelanggan → pesanan → detail_pesanan → produk. Tiap JOIN cukup menambahkan satu tabel dengan satu syarat ON:
SELECT o.id AS pesanan, pl.nama AS pelanggan, pr.nama AS produk, d.jumlah AS jumlah
FROM pesanan o
JOIN pelanggan pl ON pl.id = o.pelanggan_id
JOIN detail_pesanan d ON d.pesanan_id = o.id
JOIN produk pr ON pr.id = d.produk_id
ORDER BY o.id, pr.nama;Output yang Dihasilkan:
1|Budi|Gula Aren|1
1|Budi|Kopi Gayo|2
2|Siti|Teh Melati|3
3|Budi|Kopi Gayo|1
3|Budi|Sirup Vanila|2
4|Ani|Kopi Toraja|1
5|Siti|Gula Aren|2
5|Siti|Teh Hijau|2
6|Budi|Kopi Gayo|1Tulisan JOIN tanpa INNER berarti INNER JOIN — keduanya identik. Hasilnya satu baris per baris detail_pesanan, lengkap dengan atribut dari ketiga tabel lainnya.
LEFT JOIN: Menyelamatkan Baris Tanpa Pasangan #
LEFT JOIN mempertahankan seluruh baris tabel kiri; baris kanan yang tidak menemukan pasangan diisi NULL. Query yang sama kini menampakkan juga pelanggan tanpa pesanan:
SELECT p.nama AS pelanggan, o.id AS pesanan
FROM pelanggan p
LEFT JOIN pesanan o ON o.pelanggan_id = p.id
ORDER BY p.id;Output yang Dihasilkan:
Budi|1
Budi|3
Budi|6
Siti|2
Siti|5
Ani|4
Rina|
Dewi|Kolom pesanan untuk Rina dan Dewi kosong — bukan nol, melainkan NULL. Pola ini bisa dibalik menjadi pertanyaan “pelanggan siapa saja yang belum pernah memesan” dengan menyaring baris yang kolom kanannya NULL:
SELECT p.nama AS pelanggan
FROM pelanggan p
LEFT JOIN pesanan o ON o.pelanggan_id = p.id
WHERE o.id IS NULL
ORDER BY p.nama;Output yang Dihasilkan:
Dewi
RinaPola LEFT JOIN + IS NULL ini dikenal sebagai anti-join dan merupakan salah satu resep paling sering dipakai dalam analisis data.
Self-Join: Tabel yang Memasangkan Dengan Diri Sendiri #
Hierarki disimpan sebagai referensi ke baris lain pada tabel yang sama — misalnya setiap pegawai menyimpan atasan_id. Tabel kecil berikut melengkapi domain toko:
CREATE TABLE pegawai (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL,
atasan_id INTEGER REFERENCES pegawai(id)
);
INSERT INTO pegawai (id, nama, atasan_id) VALUES
(1, 'Budi', NULL),
(2, 'Siti', 1),
(3, 'Ani', 2),
(4, 'Rina', 2);Satu tabel ini harus dibayangkan sebagai dua salinan: salinan a untuk pegawai, salinan b untuk atasan. LEFT JOIN mempertahankan Budi yang tidak punya atasan:
SELECT a.nama AS pegawai, b.nama AS atasan
FROM pegawai a
LEFT JOIN pegawai b ON b.id = a.atasan_id
ORDER BY a.id;Output yang Dihasilkan:
Budi|
Siti|Budi
Ani|Siti
Rina|SitiCROSS JOIN dan Jebakan Kartesian #
CROSS JOIN memasangkan setiap baris tabel kiri dengan setiap baris tabel kanan — tanpa syarat ON sama sekali. Lima pelanggan dikali tujuh produk menghasilkan 35 pasangan:
SELECT COUNT(*) AS jumlah_baris
FROM pelanggan CROSS JOIN produk;Output yang Dihasilkan:
35| Jenis JOIN | Baris tanpa pasangan |
|---|---|
INNER JOIN | Dibuang dari hasil |
LEFT JOIN | Baris kiri dipertahankan, kolom kanan NULL |
CROSS JOIN | Tidak ada konsep pasangan — semua kombinasi |
Setelah tabel bisa digabungkan, pertanyaan analitis seperti “produk mana yang belum pernah terjual” bisa dijawab lewat query di dalam query — teknik yang dibahas pada Chapter Subquery.