Skema toko online sengaja memecah data ke empat tabel: nama pelanggan tidak diulang pada setiap pesanan, dan nama produk tidak diulang pada setiap baris detail. Pemisahan seperti menghemat penyimpanan dan menjaga konsistensi, tetapi pertanyaan bisnis hampir selalu melintasi tabel — “pesanan Budi berisi produk apa saja?” membutuhkan pelanggan, pesanan, detail_pesanan, dan produk sekaligus. Klausa JOIN menggabungkan tabel-tabel tersebut melalui kunci penghubung seperti pelanggan_id. Seluruh contoh memakai database toko online dari chapter Pengenalan SQL.

INNER JOIN #

INNER JOIN mengembalikan hanya baris yang memiliki pasangan pada kedua tabel, dicocokkan lewat kondisi klausa ON:

sql
SELECT pelanggan.nama, pesanan.id, pesanan.tanggal, pesanan.status
FROM pelanggan
INNER JOIN pesanan ON pelanggan.id = pesanan.pelanggan_id

Output yang Dihasilkan:

text
('Budi Santoso', 1, '2024-06-01', 'selesai')
('Siti Rahayu', 2, '2024-06-03', 'selesai')
('Budi Santoso', 3, '2024-06-10', 'dikirim')
('Dewi Lestari', 4, '2024-06-15', 'selesai')
('Rina Kartika', 5, '2024-06-20', 'dibatalkan')
('Siti Rahayu', 6, '2024-07-01', 'diproses')
('Andi Wijaya', 7, '2024-07-05', 'selesai')
('Dewi Lestari', 8, '2024-07-12', 'diproses')
('Budi Santoso', 9, '2024-07-20', 'selesai')

Mari kita telaah klausa-klausa query di atas: ON pelanggan.id = pesanan.pelanggan_id memasangkan setiap pesanan dengan pemiliknya, INNER JOIN membuang baris tanpa pasangan, dan awalan pelanggan.nama serta pesanan.id membedakan asal kolom karena kedua tabel sama-sama punya kolom id. Hasilnya sembilan baris — tepat jumlah pesanan — dan Farhan Pratama tidak muncul karena pelanggan tersebut belum pernah memesan.

LEFT JOIN dan Baris Tanpa Pasangan #

LEFT JOIN mempertahankan seluruh baris tabel kiri; baris kanan tanpa pasangan diisi NULL:

sql
SELECT pelanggan.nama, pesanan.id, pesanan.status
FROM pelanggan
LEFT JOIN pesanan ON pelanggan.id = pesanan.pelanggan_id

Output yang Dihasilkan:

text
('Budi Santoso', 3, 'dikirim')
('Budi Santoso', 1, 'selesai')
('Budi Santoso', 9, 'selesai')
('Siti Rahayu', 6, 'diproses')
('Siti Rahayu', 2, 'selesai')
('Dewi Lestari', 8, 'diproses')
('Dewi Lestari', 4, 'selesai')
('Rina Kartika', 5, 'dibatalkan')
('Andi Wijaya', 7, 'selesai')
('Farhan Pratama', None, None)

Sepuluh baris keluar: sembilan pasangan ditambah Farhan Pratama dengan kolom pesanan NULL. Perhatikan juga urutan baris yang berbeda dari query pertama tanpa ORDER BY — pengingat nyata bahwa urutan tanpa ORDER BY bukan jaminan. Pola LEFT JOIN + IS NULL menjadi alat andalan untuk mencari baris tanpa pasangan:

sql
SELECT pelanggan.nama
FROM pelanggan
LEFT JOIN pesanan ON pelanggan.id = pesanan.pelanggan_id
WHERE pesanan.id IS NULL

Output yang Dihasilkan:

text
('Farhan Pratama',)

RIGHT JOIN dan FULL OUTER JOIN #

RIGHT JOIN adalah cermin LEFT JOIN: seluruh baris tabel kanan dipertahankan. Karena setiap pesanan pasti memiliki pelanggan (kolom pelanggan_id didefinisikan NOT NULL), isi hasil berikut sama dengan INNER JOIN — hanya urutan barisnya berbeda. Pelajaran berguna: jenis JOIN hanya terlihat bedanya ketika ada baris tanpa pasangan:

sql
SELECT pelanggan.nama, pesanan.id, pesanan.status
FROM pelanggan
RIGHT JOIN pesanan ON pelanggan.id = pesanan.pelanggan_id

Output yang Dihasilkan:

text
('Budi Santoso', 1, 'selesai')
('Budi Santoso', 3, 'dikirim')
('Budi Santoso', 9, 'selesai')
('Siti Rahayu', 2, 'selesai')
('Siti Rahayu', 6, 'diproses')
('Dewi Lestari', 4, 'selesai')
('Dewi Lestari', 8, 'diproses')
('Rina Kartika', 5, 'dibatalkan')
('Andi Wijaya', 7, 'selesai')

FULL OUTER JOIN mempertahankan kedua sisi sekaligus — sembilan pesanan plus Farhan yang tanpa pasangan:

sql
SELECT pelanggan.nama, pesanan.id, pesanan.status
FROM pelanggan
FULL OUTER JOIN pesanan ON pelanggan.id = pesanan.pelanggan_id

Output yang Dihasilkan:

text
('Budi Santoso', 1, 'selesai')
('Budi Santoso', 3, 'dikirim')
('Budi Santoso', 9, 'selesai')
('Siti Rahayu', 2, 'selesai')
('Siti Rahayu', 6, 'diproses')
('Dewi Lestari', 4, 'selesai')
('Dewi Lestari', 8, 'diproses')
('Rina Kartika', 5, 'dibatalkan')
('Andi Wijaya', 7, 'selesai')
('Farhan Pratama', None, None)

Menggabungkan Empat Tabel #

Laporan lengkap “siapa membeli apa” dirangkai dari empat tabel sekaligus — setiap JOIN menambah satu tabel dengan kuncinya masing-masing:

sql
SELECT pelanggan.nama AS pelanggan,
       produk.nama AS produk,
       detail_pesanan.jumlah,
       detail_pesanan.subtotal
FROM pelanggan
JOIN pesanan ON pelanggan.id = pesanan.pelanggan_id
JOIN detail_pesanan ON pesanan.id = detail_pesanan.pesanan_id
JOIN produk ON produk.id = detail_pesanan.produk_id
WHERE pesanan.status = 'selesai'

Output yang Dihasilkan:

text
('Budi Santoso', 'Smartphone X1', 1, 3500000)
('Budi Santoso', 'Kemeja Batik', 2, 500000)
('Siti Rahayu', 'Dress Summer', 1, 380000)
('Siti Rahayu', 'TWS Earbuds', 1, 750000)
('Dewi Lestari', 'Sneakers Klasik', 1, 900000)
('Dewi Lestari', 'Kemeja Batik', 1, 250000)
('Andi Wijaya', 'Sneakers Klasik', 1, 900000)
('Budi Santoso', 'Kemeja Batik', 1, 250000)

Rantai penggabungannya berjalan bertahap: pelanggan dipasangkan dengan pesanan, hasilnya dipasangkan dengan detail_pesanan lewat pesanan.id, lalu dipasangkan lagi dengan produk lewat produk.id. Klausa WHERE pada akhirnya menyaring pesanan selesai saja, sehingga laporan hanya memuat delapan baris transaksi yang benar-benar tuntas.

Self-Join #

Tabel boleh digabungkan dengan dirinya sendiri — cukup beri alias berbeda pada tiap kemunculan. Query berikut menemukan pasangan pelanggan yang tinggal di kota sama:

sql
SELECT p1.nama AS pelanggan_a, p2.nama AS pelanggan_b, p1.kota
FROM pelanggan p1
JOIN pelanggan p2 ON p1.kota = p2.kota AND p1.id < p2.id

Output yang Dihasilkan:

text
('Budi Santoso', 'Rina Kartika', 'Jakarta')
('Siti Rahayu', 'Farhan Pratama', 'Bandung')

Alias p1 dan p2 memperlakukan satu tabel fisik sebagai dua sumber data. Kondisi p1.id < p2.id mencegah dua keanehan: pasangan dengan diri sendiri (p1.id = p2.id) dan pasangan ganda seperti Budi-Rina sekaligus Rina-Budi. Andi Wijaya tidak muncul karena perbandingan NULL = NULL tidak pernah benar — kota Andi kosong.

Jebakan Fan-Out pada JOIN #

Satu pesanan memiliki banyak baris detail, sehingga penggabungan pesanan dengan detail menggandakan baris pesanan — efek yang dikenal sebagai fan-out:

sql
SELECT COUNT(*) AS total_baris,
       COUNT(DISTINCT pesanan.id) AS jumlah_pesanan
FROM pesanan
JOIN detail_pesanan ON pesanan.id = detail_pesanan.pesanan_id

Output yang Dihasilkan:

text
(11, 8)

Sebelas baris hasil join berasal dari delapan pesanan — pesanan 1 dan 4 masing-masing muncul dua kali karena memuat dua produk. COUNT(*) menghitung baris join, bukan pesanan; hanya COUNT(DISTINCT pesanan.id) yang menghitung dengan benar.

Dengan JOIN, kita menggabungkan data secara horizontal — menambah kolom dari tabel lain. Chapter berikutnya membahas teknik yang berbeda: memakai hasil satu query sebagai bahan query lain, dari subquery skalar hingga EXISTS, pada chapter Subquery.