Pelajari 6 JOIN SQL yang harus dikuasai—INNER, LEFT, RIGHT, FULL OUTER, CROSS, dan SELF—dengan contoh praktis dan jebakan umum.

Sebuah SQL JOIN memungkinkan Anda menggabungkan baris dari dua (atau lebih) tabel menjadi satu hasil dengan mencocokkannya pada kolom terkait—biasanya sebuah ID.
Kebanyakan basis data nyata sengaja dibagi ke tabel yang terpisah agar Anda tidak mengulang informasi yang sama berulang kali. Misalnya, nama pelanggan berada di tabel customers, sedangkan pembeliannya berada di tabel orders. JOIN adalah cara Anda menghubungkan kembali potongan-potongan itu saat Anda butuh jawaban.
Itulah alasan JOIN muncul di mana-mana dalam pelaporan dan analisis:
Tanpa JOIN, Anda akan terjebak menjalankan query terpisah dan menggabungkan hasil secara manual—lambat, rawan kesalahan, dan sulit diulang.
Jika Anda membangun produk di atas basis data relasional (dashboard, panel admin, alat internal, portal pelanggan), JOIN juga yang mengubah “tabel mentah” menjadi tampilan yang dapat digunakan pengguna. Platform seperti Koder.ai (yang menghasilkan aplikasi React + Go + PostgreSQL dari chat) tetap bergantung pada dasar JOIN ketika Anda butuh halaman daftar, laporan, dan layar rekonsiliasi yang akurat—karena logika basis data tidak hilang walau pengembangan jadi lebih cepat.
Panduan ini fokus pada enam JOIN yang mencakup sebagian besar pekerjaan SQL sehari-hari:
Sintaks JOIN sangat mirip di sebagian besar DBMS (PostgreSQL, MySQL, SQL Server, SQLite). Ada beberapa perbedaan—terutama dukungan FULL OUTER JOIN dan beberapa perilaku edge-case—tetapi konsep dan pola inti mudah dipindahkan antar platform.
Untuk menyederhanakan contoh JOIN, kita akan menggunakan tiga tabel kecil yang mencerminkan setup dunia nyata: customers membuat orders, dan orders mungkin (atau mungkin tidak) memiliki payments.
Catatan kecil sebelum mulai: tabel contoh di bawah hanya menunjukkan beberapa kolom, tetapi beberapa query nanti merujuk kolom tambahan (seperti order_date, created_at, status, atau paid_at) untuk menunjukkan pola umum. Anggap kolom-kolom itu sebagai field “tipikal” yang sering ada di skema produksi.
Primary key: customer_id
| customer_id | name |
|---|---|
| 1 |
Primary key: order_id
Foreign key: customer_id → customers.customer_id
Perhatikan order_id = 104 merujuk customer_id = 5, yang tidak ada di customers. Kasus “kecocokan hilang” ini berguna untuk melihat bagaimana LEFT JOIN, RIGHT JOIN, dan FULL OUTER JOIN berperilaku.
Primary key: payment_id
Foreign key: order_id → orders.order_id
Dua detail pengajaran penting di sini:
order_id = 102 punya dua baris pembayaran (pembayaran terpisah). Saat Anda join orders ke payments, order itu akan muncul dua kali—di sinilah duplikasi sering mengejutkan orang.payment_id = 9004 merujuk order_id = 999, yang tidak ada di . Itu menciptakan kasus “tidak cocok” lain.orders ke payments akan mengulangi order 102 karena punya dua pembayaran.INNER JOIN mengembalikan hanya baris di mana ada kecocokan di kedua tabel. Jika seorang pelanggan tidak punya pesanan, mereka tidak akan muncul di hasil. Jika sebuah order merujuk ke customer yang tidak ada (data buruk), order itu juga tidak akan muncul.
Anda memilih tabel “kiri”, join tabel “kanan”, dan menghubungkannya dengan kondisi di klausa ON.
SELECT
c.customer_id,
c.name,
o.order_id,
o.order_date
FROM customers c
INNER JOIN orders o
ON o.customer_id = c.customer_id;
Ide kuncinya adalah baris ON o.customer_id = c.customer_id: itu memberi tahu SQL bagaimana baris berkaitan.
Jika Anda ingin daftar hanya pelanggan yang benar-benar membuat setidaknya satu pesanan (dan detail pesanan), INNER JOIN adalah pilihan natural:
SELECT
c.name,
o.order_id,
o.total_amount
FROM customers c
INNER JOIN orders o
ON o.customer_id = c.customer_id
ORDER BY o.order_id;
Ini berguna untuk hal-hal seperti “kirim email tindak lanjut pesanan” atau “hitung pendapatan per pelanggan” (ketika Anda hanya peduli tentang pelanggan yang bertransaksi).
Jika Anda menulis join tapi lupa kondisi ON (atau join pada kolom yang salah), Anda bisa tak sengaja membuat produk Kartesius (setiap customer digabungkan dengan setiap order) atau menghasilkan kecocokan yang salah secara halus.
Buruk (jangan lakukan ini):
SELECT c.name, o.order_id
FROM customers c
JOIN orders o;
Selalu pastikan Anda punya kondisi join yang jelas di ON (atau USING pada kasus tertentu—akan dibahas nanti).
LEFT JOIN mengembalikan semua baris dari tabel kiri, dan menambahkan data yang cocok dari tabel kanan jika ada. Jika tidak ada kecocokan, kolom sisi kanan akan bernilai NULL.
Gunakan LEFT JOIN ketika Anda menginginkan daftar lengkap dari tabel utama Anda, plus data terkait opsional.
Contoh: “Tunjukkan semua pelanggan, dan sertakan pesanan mereka jika ada.”
SELECT
c.customer_id,
c.name,
o.order_id,
o.order_date
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id
ORDER BY c.customer_id;
o.order_id (dan kolom orders lainnya) akan NULL.Alasan sangat umum menggunakan LEFT JOIN adalah untuk menemukan item yang tidak memiliki record terkait.
Contoh: “Pelanggan mana yang tidak pernah membuat pesanan?”
SELECT
c.customer_id,
c.name
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id
WHERE o.order_id IS NULL;
Kondisi WHERE ... IS NULL itu menjaga hanya baris tabel kiri yang tidak ditemukan kecocokan.
LEFT JOIN bisa “menduplikasi” baris tabel kiri ketika ada banyak baris cocok di kanan.
Jika satu pelanggan punya 3 pesanan, pelanggan itu akan muncul 3 kali—satu kali per pesanan. Itu ekspektasi yang benar, tapi bisa mengejutkan jika Anda sedang mencoba menghitung pelanggan.
Contoh, ini menghitung pesanan (bukan pelanggan):
SELECT COUNT(*)
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id;
Jika tujuan Anda menghitung pelanggan, biasanya Anda menghitung kunci pelanggan instead (sering dengan COUNT(DISTINCT c.customer_id)), tergantung apa yang ingin Anda ukur.
RIGHT JOIN mempertahankan semua baris dari tabel kanan, dan hanya baris yang cocok dari tabel kiri. Jika tidak ada kecocokan, kolom tabel kiri akan NULL. Ini pada dasarnya adalah cermin dari LEFT JOIN.
Dengan tabel contoh kita, bayangkan Anda ingin menampilkan semua pembayaran, bahkan jika tidak dapat dihubungkan ke order (mungkin order dihapus, atau data pembayaran berantakan).
SELECT
o.order_id,
o.customer_id,
p.payment_id,
p.amount,
p.paid_at
FROM orders o
RIGHT JOIN payments p
ON o.order_id = p.order_id;
Yang Anda dapatkan:
payments ada di kanan).o.order_id dan o.customer_id akan NULL.Sebagian besar waktu, Anda bisa menulis ulang RIGHT JOIN sebagai LEFT JOIN dengan menukar urutan tabel:
SELECT
o.order_id,
o.customer_id,
p.payment_id,
p.amount,
p.paid_at
FROM payments p
LEFT JOIN orders o
ON o.order_id = p.order_id;
Ini mengembalikan hasil yang sama, tetapi banyak orang merasa lebih mudah dibaca: Anda mulai dengan tabel “utama” yang Anda pedulikan (di sini, payments) lalu “opsional” menarik data terkait.
Banyak pedoman gaya SQL menyarankan menghindari RIGHT JOIN karena memaksa pembaca membalik pola yang umum:
Ketika relasi opsional selalu ditulis sebagai LEFT JOIN, query jadi lebih mudah di-scan.
RIGHT JOIN bisa berguna saat Anda mengedit query yang sudah ada dan menyadari tabel “harus-dipertahankan” sedang berada di kanan. Daripada menulis ulang seluruh query (terutama jika panjang dengan banyak join), mengganti satu join menjadi RIGHT JOIN bisa jadi perubahan cepat dan berisiko rendah.
FULL OUTER JOIN mengembalikan semua baris dari kedua tabel.
INNER JOIN).NULL pada kolom tabel kanan.NULL pada kolom tabel kiri.Kasus bisnis klasik adalah merekonciliikan orders vs. payments:
Contoh:
SELECT
o.order_id,
o.customer_id,
p.payment_id,
p.amount
FROM orders o
FULL OUTER JOIN payments p
ON p.order_id = o.order_id;
FULL OUTER JOIN didukung di PostgreSQL, SQL Server, dan Oracle.
Tidak tersedia di MySQL dan SQLite (Anda perlu solusi pengganti).
Jika database Anda tidak mendukung FULL OUTER JOIN, Anda bisa mensimulasikannya dengan menggabungkan:
orders (dengan pembayaran yang cocok bila tersedia), danpayments yang tidak cocok ke order.Satu pola umum:
SELECT o.order_id, o.customer_id, p.payment_id, p.amount
FROM orders o
LEFT JOIN payments p
ON p.order_id = o.order_id
UNION
SELECT o.order_id, o.customer_id, p.payment_id, p.amount
FROM orders o
RIGHT JOIN payments p
ON p.order_id = o.order_id;
Tip: ketika Anda melihat NULL di satu sisi, itu sinyal bahwa baris itu “hilang” di tabel lain — persis yang Anda inginkan untuk audit dan rekonsiliasi.
CROSS JOIN mengembalikan setiap pasangan kemungkinan baris dari dua tabel. Jika tabel A punya 3 baris dan tabel B punya 4 baris, hasilnya akan punya 3 × 4 = 12 baris. Ini juga disebut produk Kartesius.
Kedengarannya menakutkan—dan memang bisa—tetapi berguna ketika Anda ingin menghasilkan kombinasi.
Bayangkan Anda menyimpan opsi produk di tabel terpisah:
sizes: S, M, Lcolors: Red, BlueCROSS JOIN dapat menghasilkan semua varian (berguna untuk membuat SKU, pra-membangun katalog, atau pengujian):
SELECT
s.size,
c.color
FROM sizes AS s
CROSS JOIN colors AS c;
Hasil (3 × 2 = 6 baris):
Karena jumlah baris berlipat, CROSS JOIN bisa meledak dengan cepat:
Itu bisa memperlambat query, membebani memori, dan menghasilkan keluaran yang tidak berguna. Jika Anda butuh kombinasi, jaga input kecil dan pertimbangkan menambahkan limit atau filter secara terkendali.
SELF JOIN adalah persis apa yang namanya: Anda menggabungkan tabel dengan dirinya sendiri. Ini berguna ketika satu baris dalam tabel berhubungan dengan baris lain di tabel yang sama—paling umum untuk hubungan parent/child seperti karyawan dan manager.
Karena Anda menggunakan tabel yang sama dua kali, Anda harus memberi setiap “salinan” alias berbeda. Alias membuat query terbaca dan memberi tahu SQL sisi mana yang dimaksud.
Pola umum:
e untuk employeem untuk managerBayangkan tabel employees seperti ini:
idnamemanager_id (mengarah ke id employee lain)Untuk menampilkan setiap karyawan dengan nama managernya:
SELECT
e.id,
e.name AS employee_name,
m.name AS manager_name
FROM employees e
LEFT JOIN employees m
ON e.manager_id = m.id;
Perhatikan query menggunakan LEFT JOIN, bukan INNER JOIN. Itu penting karena beberapa karyawan mungkin tidak punya manager (mis. CEO). Dalam kasus itu, manager_id sering NULL, dan LEFT JOIN mempertahankan baris karyawan sambil menampilkan manager_name sebagai NULL.
Jika Anda menggunakan INNER JOIN, karyawan level atas itu akan hilang dari hasil karena tidak ada baris manager untuk di-join.
JOIN tidak “tahu” sendiri bagaimana dua tabel berhubungan—Anda harus memberitahukannya. Hubungan itu didefinisikan dalam kondisi join, dan sebaiknya berada tepat di sebelah klausa JOIN karena menjelaskan bagaimana tabel cocok, bukan bagaimana Anda ingin memfilter hasil akhir.
ON: paling fleksibel (dan paling umum)Gunakan ON saat Anda ingin kontrol penuh atas logika pencocokan—nama kolom berbeda, kondisi ganda, atau aturan tambahan.
SELECT
c.customer_id,
c.name,
o.order_id,
o.created_at
FROM customers AS c
INNER JOIN orders AS o
ON o.customer_id = c.customer_id;
ON juga tempat Anda dapat mendefinisikan pencocokan yang lebih kompleks (mis. mencocokkan dua kolom) tanpa membuat query menjadi tebakan-tebakan.
USING: lebih singkat, tapi hanya untuk kolom bernama samaBeberapa DB (seperti PostgreSQL dan MySQL) mendukung USING. Ini adalah shorthand yang nyaman ketika kedua tabel punya kolom dengan nama yang sama dan Anda ingin join pada kolom itu.
SELECT
customer_id,
name,
order_id
FROM customers
JOIN orders
USING (customer_id);
Salah satu keuntungan: USING biasanya mengembalikan hanya satu kolom customer_id di output (alih-alih dua salinan).
Setelah Anda join tabel, nama kolom sering tumpang tindih (id, created_at, status). Jika Anda menulis SELECT id, database mungkin memberi error “ambiguous column”—atau lebih buruk, Anda bisa membaca id yang salah.
Utamakan prefix tabel (atau alias) untuk kejelasan:
SELECT c.customer_id, o.order_id
FROM customers AS c
JOIN orders AS o
ON o.customer_id = c.customer_id;
SELECT * dalam query yang di-joinSELECT * cepat menjadi berantakan dengan join: Anda menarik kolom yang tidak diperlukan, risiko nama ganda, dan menyulitkan pemahaman tujuan query. Sebagai gantinya, pilih kolom yang benar-benar Anda butuhkan. Hasil lebih bersih, lebih mudah dipelihara, dan sering lebih efisien—terutama saat tabel lebar.
Saat Anda join tabel, WHERE dan ON keduanya “memfilter”, tetapi mereka melakukannya pada momen berbeda.
Perbedaan timing itu penyebab orang sering tidak sengaja mengubah LEFT JOIN menjadi INNER JOIN.
Katakan Anda ingin semua pelanggan, bahkan yang tidak punya pesanan berbayar terbaru.
SELECT c.customer_id, c.name, o.order_id, o.status, o.order_date
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id
WHERE o.status = 'PAID'
AND o.order_date >= DATE '2025-01-01';
Masalah: untuk pelanggan tanpa order yang cocok, o.status dan o.order_date adalah NULL. Klausa WHERE menolak baris-barus itu, sehingga pelanggan tanpa kecocokan hilang—LEFT JOIN Anda berperilaku seperti INNER JOIN.
SELECT c.customer_id, c.name, o.order_id, o.status, o.order_date
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id
AND o.status = 'PAID'
AND o.order_date >= DATE '2025-01-01';
Sekarang pelanggan tanpa pesanan yang memenuhi syarat tetap muncul (dengan kolom order NULL), yang biasanya tujuan LEFT JOIN.
Join tidak hanya “menambahkan kolom”—mereka juga bisa mengalikan baris. Itu biasanya perilaku yang benar, tetapi sering mengejutkan ketika total tiba-tiba berlipat ganda (atau lebih buruk).
Sebuah join mengembalikan satu baris output untuk setiap pasangan baris yang cocok.
customers ke orders, setiap pelanggan mungkin muncul berkali-kali—sekali per order.Jika tujuan Anda “satu baris per pelanggan” atau “satu baris per order”, ringkas sisi "many" terlebih dahulu, lalu join.
-- One row per order from payments
WITH payment_totals AS (
order_id,
(amount) total_paid,
() payment_count
payments
order_id
)
o.order_id,
o.customer_id,
(pt.total_paid, ) total_paid,
(pt.payment_count, ) payment_count
orders o
payment_totals pt
pt.order_id o.order_id;
Ini menjaga bentuk join tetap dapat diprediksi: satu baris order tetap satu baris order.
SELECT DISTINCT bisa membuat duplikasi terlihat beres, tetapi mungkin menyembunyikan masalah sebenarnya:
Gunakan hanya ketika Anda yakin duplikasi murni tidak sengaja dan Anda mengerti penyebabnya.
Sebelum mempercayai hasil, bandingkan jumlah baris:
JOIN sering disalahkan atas “query lambat”, tetapi penyebab nyata biasanya adalah berapa banyak data yang Anda minta untuk digabungkan, dan seberapa mudah database menemukan baris yang cocok.
Pikirkan index seperti daftar isi buku. Tanpa index, database mungkin harus memindai banyak baris untuk menemukan kecocokan pada kondisi JOIN Anda. Dengan index pada key join (mis. customers.customer_id dan orders.customer_id), database bisa langsung melompat ke baris relevan jauh lebih cepat.
Anda tidak perlu tahu detail internalnya untuk memakai ini dengan baik: jika sebuah kolom sering dipakai untuk mencocokkan baris (ON a.id = b.a_id), itu kandidat bagus untuk di-index.
Sebisa mungkin, join pada identifier unik yang stabil:
customers.customer_id = orders.customer_idcustomers.email = orders.email atau customers.name = orders.nameNama berubah dan bisa sama. Email bisa berubah, hilang, atau berbeda format. ID dirancang khusus untuk pencocokan konsisten, dan biasanya di-index.
Dua kebiasaan membuat JOIN terasa lebih cepat:
SELECT * saat join—kolom ekstra meningkatkan penggunaan memori dan jaringan.Contoh: batasi orders dulu, lalu join:
SELECT c.customer_id, c.name, o.order_id, o.created_at
FROM customers c
JOIN (
SELECT order_id, customer_id, created_at
FROM orders
WHERE created_at >= DATE '2025-01-01'
) o
ON o.customer_id = c.customer_id;
Jika Anda iterasi pada query ini dalam proses membangun aplikasi (mis. membuat halaman laporan dengan PostgreSQL), alat seperti Koder.ai bisa mempercepat scaffolding—skema, endpoint, UI—sementara Anda tetap mengendalikan logika JOIN yang menentukan kebenaran.
Sebuah SQL JOIN menggabungkan baris dari dua (atau lebih) tabel menjadi satu hasil dengan mencocokkan kolom terkait—biasanya primary key ke foreign key (misalnya, customers.customer_id = orders.customer_id). Ini adalah cara untuk “menghubungkan kembali” tabel yang dinormalisasi saat Anda butuh laporan, audit, atau analisis.
Gunakan INNER JOIN saat Anda hanya menginginkan baris yang hubungan-nya ada di kedua tabel.
Ini ideal untuk “hubungan yang terkonfirmasi”, misalnya menampilkan hanya pelanggan yang benar-benar melakukan pembelian.
Gunakan LEFT JOIN ketika Anda butuh semua baris dari tabel utama (kiri) dan menambahkan data terkait dari tabel kanan jika ada.
Untuk menemukan “tidak ada kecocokan”, gabungkan lalu saring sisi kanan menjadi NULL:
c.customer_id, c.name
customers c
orders o o.customer_id c.customer_id
o.order_id ;
RIGHT JOIN menyertakan setiap baris dari tabel kanan dan mengisi kolom tabel kiri dengan NULL ketika tidak ada kecocokan. Banyak tim menghindarinya karena membaca “terbalik”.
Dalam kebanyakan kasus, Anda bisa menulis ulang sebagai LEFT JOIN dengan menukar urutan tabel:
FROM payments p
orders o o.order_id p.order_id
Gunakan FULL OUTER JOIN untuk rekonsiliasi: Anda ingin melihat yang cocok, yang hanya ada di kiri, dan yang hanya ada di kanan dalam satu keluaran.
Ini bagus untuk audit seperti “pesanan tanpa pembayaran” dan “pembayaran tanpa pesanan”, karena sisi yang tidak cocok akan tampil sebagai kolom NULL.
Beberapa basis data (terutama MySQL dan SQLite) tidak mendukung FULL OUTER JOIN langsung. Cara umum adalah menggabungkan dua query:
orders LEFT JOIN paymentsBiasanya dilakukan dengan UNION (atau UNION ALL dengan penyaringan yang hati-hati) sehingga Anda mempertahankan record “kiri-saja” dan “kanan-saja”.
CROSS JOIN menghasilkan setiap kombinasi baris antara dua tabel (produk Kartesius). Berguna untuk menghasilkan skenario (mis. ukuran × warna) atau membuat grid kalender.
Hati-hati: jumlah baris cepat membengkak, jadi ini hanya praktis jika inputnya kecil dan terkontrol.
Self join adalah menggabungkan tabel ke dirinya sendiri untuk mengaitkan baris di dalam tabel yang sama (umum untuk hirarki seperti karyawan → manager).
Anda harus menggunakan alias untuk membedakan dua “salinan”:
FROM employees e
LEFT JOIN employees m
ON e.manager_id = m.id
ON mendefinisikan bagaimana baris dicocokkan saat join; WHERE memfilter setelah hasil join terbentuk. Dengan LEFT JOIN, kondisi WHERE pada tabel kanan bisa menghapus baris NULL yang membuatnya berperilaku seperti INNER JOIN.
Jika Anda ingin mempertahankan semua baris kiri namun membatasi baris kanan yang boleh cocok, taruh kondisi pada tabel kanan di .
Join dapat menggandakan baris ketika relasinya one-to-many atau many-to-many. Misalnya, sebuah pesanan dengan dua pembayaran akan muncul dua kali jika Anda menggabungkan orders ke payments.
Untuk menjaga “satu baris per order/pelanggan”, lakukan agregasi dulu di sisi many (mis. SUM(amount) grouped by order_id) lalu join. Gunakan DISTINCT hanya sebagai upaya terakhir karena bisa menyembunyikan masalah join dan merusak total.
| Ava |
| 2 | Ben |
| 3 | Chen |
| 4 | Dia |
| order_id | customer_id | order_total |
|---|
| 101 | 1 | 50 |
| 102 | 1 | 120 |
| 103 | 2 | 35 |
| 104 | 5 | 70 |
| payment_id | order_id | amount |
|---|
| 9001 | 101 | 50 |
| 9002 | 102 | 60 |
| 9003 | 102 | 60 |
| 9004 | 999 | 25 |
ordersWHERE o.order_id IS NOT NULL secara eksplisit).orderspaymentsorder_itemspayments × itemsNULL)NULL ketika hilang)NULLON