Cara JOIN Lebih dari Dua Tabel di SQL Tanpa Bikin Duplikat
TL;DR
JOIN lebih dari dua tabel di SQL dilakukan dengan menumpuk klausa JOIN secara berurutan, di mana setiap JOIN baru menyambung ke hasil gabungan sebelumnya. Masalah paling sering muncul saat salah satu tabel punya relasi one-to-many, karena satu baris bisa berlipat jadi beberapa baris dan bikin angka SUM jadi berkali lipat. Solusinya: cek jumlah baris tiap kali nambah JOIN, dan agregat tabel yang one-to-many jadi satu baris per kunci lewat subquery sebelum digabung.
Nambah JOIN keempat, total omzet tiba-tiba naik jadi tiga kali lipat. Datanya gak berubah. Yang berubah cuma jumlah barisnya.
Ini bug SQL yang paling mahal, soalnya dia gak ngasih error. Query-nya jalan mulus, angkanya keluar, dan kamu baru sadar salah pas laporan udah dikirim ke bos.
Di bawah ini penyebabnya, cara ngedeteksinya dalam 10 detik, dan cara benerinnya.
Gimana cara JOIN lebih dari dua tabel di SQL?
Caranya numpuk klausa JOIN satu per satu. Tiap JOIN baru nyambung ke hasil gabungan sebelumnya, bukan ke tabel awal doang.
SELECT ...
FROM tabel_a a
JOIN tabel_b b ON b.a_id = a.id
JOIN tabel_c c ON c.b_id = b.id
JOIN tabel_d d ON d.c_id = c.id;
Database bacanya dari atas ke bawah. tabel_c boleh nyambung ke a atau ke b — dua-duanya udah tersedia di titik itu.
Yang gak boleh: nyambung ke tabel yang belum disebut di baris sebelumnya.
Contoh kasus: 4 tabel di toko_berkah
Struktur datanya:
kota— daftar kotapelanggan— nyambung ke kotapesanan— nyambung ke pelanggan, punya kolomongkirpesanan_item— nyambung ke pesanan, satu pesanan bisa punya banyak item
Bikin tabelnya:
CREATE TABLE kota (
kota_id INT PRIMARY KEY,
nama_kota VARCHAR(50) NOT NULL
);
CREATE TABLE pelanggan (
pelanggan_id INT PRIMARY KEY,
nama VARCHAR(100) NOT NULL,
kota_id INT
);
CREATE TABLE pesanan (
pesanan_id INT PRIMARY KEY,
pelanggan_id INT,
tanggal DATE,
ongkir DECIMAL(12,2)
);
CREATE TABLE pesanan_item (
item_id INT PRIMARY KEY,
pesanan_id INT,
nama_produk VARCHAR(100),
qty INT,
harga DECIMAL(12,2)
);
Isi datanya:
INSERT INTO kota (kota_id, nama_kota) VALUES
(1, 'Bandung'),
(2, 'Cimahi');
INSERT INTO pelanggan (pelanggan_id, nama, kota_id) VALUES
(10, 'Warung Bu Siti', 1),
(11, 'Toko Pak Anwar', 2);
INSERT INTO pesanan (pesanan_id, pelanggan_id, tanggal, ongkir) VALUES
(100, 10, '2026-01-05', 20000),
(101, 11, '2026-01-06', 15000);
INSERT INTO pesanan_item (item_id, pesanan_id, nama_produk, qty, harga) VALUES
(1, 100, 'Beras 5kg', 2, 78000),
(2, 100, 'Minyak Goreng 2L',1, 36000),
(3, 100, 'Kopi Gayo 250g', 1, 65000),
(4, 101, 'Gula Aren 500g', 3, 28000);
Perhatiin: pesanan 100 punya 3 item, tapi ongkirnya cuma satu — Rp 20.000, dibayar sekali.
Query yang salah
SELECT
k.nama_kota,
SUM(i.qty * i.harga) AS omzet,
SUM(p.ongkir) AS total_ongkir
FROM pesanan p
JOIN pelanggan pl ON pl.pelanggan_id = p.pelanggan_id
JOIN kota k ON k.kota_id = pl.kota_id
JOIN pesanan_item i ON i.pesanan_id = p.pesanan_id
GROUP BY k.nama_kota;
Hasilnya:
| nama_kota | omzet | total_ongkir |
|---|---|---|
| Bandung | 257.000 | 60.000 |
| Cimahi | 84.000 | 15.000 |
Omzet Bandung bener. Tapi ongkirnya Rp 60.000, padahal aslinya cuma Rp 20.000.
Kenapa? Waktu pesanan di-JOIN ke pesanan_item, satu baris pesanan berlipat jadi 3 baris — satu buat tiap item. Kolom ongkir ikut kegandain, dan SUM ngitung 20.000 sebanyak tiga kali.
Selisih Rp 40.000 di data mainan ini. Di data produksi 50.000 pesanan? Bisa ratusan juta.
Gimana cara ngedeteksi baris kegandain?
Cara paling cepat: hitung barisnya tiap kali nambah JOIN.
SELECT COUNT(*) FROM pesanan;
-- 2
SELECT COUNT(*) FROM pesanan p
JOIN pelanggan pl ON pl.pelanggan_id = p.pelanggan_id;
-- 2 (aman, gak nambah)
SELECT COUNT(*) FROM pesanan p
JOIN pelanggan pl ON pl.pelanggan_id = p.pelanggan_id
JOIN pesanan_item i ON i.pesanan_id = p.pesanan_id;
-- 4 (naik! ini biang keroknya)
Angka naik dari 2 ke 4 padahal kamu cuma mau nambah kolom nama produk. Itu sinyalnya.
Cari kunci yang gak unik
SELECT pesanan_id, COUNT(*) AS jumlah_baris
FROM pesanan_item
GROUP BY pesanan_id
HAVING COUNT(*) > 1;
Keluar: pesanan_id = 100, jumlah baris 3. Ketemu.
Cara benerin: agregat dulu, baru JOIN
Kalau tabelnya one-to-many, ubah dia jadi one-to-one duluan. Bungkus di subquery, agregat pakai GROUP BY, baru gabungin.
SELECT
k.nama_kota,
SUM(item.omzet) AS omzet,
SUM(p.ongkir) AS total_ongkir
FROM pesanan p
JOIN pelanggan pl ON pl.pelanggan_id = p.pelanggan_id
JOIN kota k ON k.kota_id = pl.kota_id
JOIN (
SELECT pesanan_id, SUM(qty * harga) AS omzet
FROM pesanan_item
GROUP BY pesanan_id
) item ON item.pesanan_id = p.pesanan_id
GROUP BY k.nama_kota;
Subquery-nya nge-ringkes pesanan_item jadi satu baris per pesanan, dengan omzetnya udah dijumlah duluan.
Hasilnya sekarang:
| nama_kota | omzet | total_ongkir |
|---|---|---|
| Bandung | 257.000 | 20.000 |
| Cimahi | 84.000 | 15.000 |
Ongkirnya bener. Omzetnya juga tetap bener.
Versi CTE, biar lebih enak dibaca
WITH omzet_per_pesanan AS (
SELECT pesanan_id, SUM(qty * harga) AS omzet
FROM pesanan_item
GROUP BY pesanan_id
)
SELECT
k.nama_kota,
SUM(o.omzet) AS omzet,
SUM(p.ongkir) AS total_ongkir
FROM pesanan p
JOIN pelanggan pl ON pl.pelanggan_id = p.pelanggan_id
JOIN kota k ON k.kota_id = pl.kota_id
JOIN omzet_per_pesanan o ON o.pesanan_id = p.pesanan_id
GROUP BY k.nama_kota;
Sama persis hasilnya. Tapi tiap langkah punya nama, jadi orang lain bisa ngerti dalam 5 detik.
Sintaks CTE lengkapnya ada di dokumentasi WITH queries PostgreSQL.
Kesalahan umum
1. Nyampur INNER JOIN sama LEFT JOIN sembarangan
Urutannya penting. LEFT JOIN yang ditaruh setelah INNER JOIN bisa kehilangan baris yang tadinya mau kamu pertahanin.
Aturannya: tabel utama di FROM, terus LEFT JOIN semua tabel tambahan sesudahnya.
2. Nyambung ke tabel yang salah
-- Salah: item nyambung ke pelanggan, bukan ke pesanan
JOIN pesanan_item i ON i.pesanan_id = pl.pelanggan_id
Kolomnya sama-sama INT, jadi gak error. Tapi hasilnya ngaco total. Selalu baca ulang kolom di dua sisi ON.
3. SELECT * di query multi-tabel
Kolom id muncul 4 kali dengan nama sama. Bingung sendiri nanti. Tulis kolom yang kamu butuh, kasih alias yang jelas.
4. Numpuk 8 tabel dalam satu query
Lewat 5–6 tabel, optimizer mulai kesulitan nyari rencana eksekusi yang bagus, dan kamu kesulitan baca query-nya sendiri. Pecah pakai CTE.
5. Percaya sama angkanya tanpa cross-check
Total omzet dari query 4 tabel harus sama dengan total omzet dari query 1 tabel pesanan_item. Kalau beda, ada yang kegandain. Cek 30 detik, hemat sehari.
Checklist sebelum kirim laporan
SELECT COUNT(*)sebelum dan sesudah tiap JOIN — naik gak?- Cek kunci join pakai
GROUP BY ... HAVING COUNT(*) > 1 - Bandingin total agregat sama query sederhana satu tabel
- Kolom yang cuma punya satu nilai per entitas (ongkir, diskon pesanan) — jangan di-SUM setelah JOIN one-to-many
FAQ
Berapa banyak tabel yang boleh di-JOIN dalam satu query?
Secara teknis, PostgreSQL dan SQL Server sanggup nanganin puluhan tabel dalam satu query. Tapi begitu lewat 5 sampai 6 tabel, query-nya jadi susah dibaca dan optimizer mulai kesulitan nyari rencana eksekusi terbaik. Kalau butuh lebih banyak, pecah pakai CTE supaya tiap langkah punya nama dan bisa dicek sendiri-sendiri.
Kenapa total omzet jadi berlipat setelah nambah JOIN?
Karena salah satu tabel yang kamu JOIN punya relasi one-to-many. Satu pesanan yang isinya 3 item bakal jadi 3 baris setelah di-JOIN ke tabel item. Kolom yang cuma ada satu nilai per pesanan, misalnya ongkir, jadi ikut kehitung 3 kali waktu di-SUM. Solusinya agregat tabel item dulu jadi satu baris per pesanan.
Gimana cara ngecek query saya bikin duplikat atau nggak?
Jalanin SELECT COUNT(*) sebelum dan sesudah setiap JOIN ditambahin. Kalau angkanya naik padahal kamu cuma mau nambah kolom deskriptif, berarti ada relasi one-to-many yang bikin baris berlipat. Cara kedua, cek kunci join-nya pakai GROUP BY kolom_kunci HAVING COUNT(*) > 1.
Apakah urutan JOIN mempengaruhi hasil?
Buat INNER JOIN, urutannya gak ngubah hasil akhir, soalnya optimizer bebas nyusun ulang. Tapi buat LEFT JOIN, urutannya penting banget. LEFT JOIN yang ditaruh setelah INNER JOIN bisa kehilangan baris yang tadinya mau dipertahankan. Taruh tabel utama di FROM, terus LEFT JOIN semua tabel tambahan sesudahnya.
Lebih bagus pakai subquery atau CTE buat JOIN banyak tabel?
CTE lebih enak dibaca kalau query-nya panjang, soalnya tiap langkah punya nama dan bisa dites terpisah. Subquery lebih pendek buat kasus sederhana yang cuma butuh satu agregasi. Dari sisi kecepatan, di PostgreSQL modern dan SQL Server keduanya biasanya menghasilkan rencana eksekusi yang mirip.
Penutup
Tiga hal yang nyelametin laporan kamu:
- Baris berlipat gak pernah kasih error. Cek
COUNT(*)tiap nambah JOIN. - Tabel one-to-many? Agregat dulu di subquery, baru gabungin.
- Kolom kayak ongkir dan diskon pesanan cuma boleh di-SUM setelah barisnya udah gak kegandain.
Semua query di atas bisa langsung kamu copy dan jalanin. Coba dua-duanya, terus liat selisih ongkirnya sendiri.
Mau latihan JOIN bertingkat sama data yang lebih gede? Ada di halaman fungsi JOIN. Istilah one-to-many aku jelasin di glosarium relasi one-to-many.
Lanjut baca: EXISTS SQL — cara ngecek keberadaan data tanpa bikin baris berlipat sama sekali.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Artikel Terkait
7 SQL Client Gratis Terbaik buat Analis Data (2026)
Nggak perlu bayar buat nulis query. Ini 7 SQL client gratis yang beneran dipakai analis, plus kelebihan dan kekurangannya masing-masing.
SQL Style Guide: Cara Nulis Query yang Enak Dibaca Tim
Sepuluh aturan SQL style guide yang bikin query kamu kebaca sama tim — dan sama kamu sendiri 6 bulan lagi. Lengkap dengan contoh sebelum-sesudah.
Take-Home Test SQL Data Analyst: Contoh Soal + Cara Ngerjainnya
Contoh soal take-home test SQL yang beneran dipakai perusahaan, dikerjain step by step — dari baca soal, nulis query, sampai nyusun insight yang bikin recruiter nengok.