Kenapa JOIN Melipatgandakan Baris dan Cara Mendeteksinya
TL;DR
JOIN melipatgandakan baris kalau satu baris di tabel kiri cocok sama lebih dari satu baris di tabel kanan, dan ini terjadi diam-diam tanpa pesan error. Akibatnya SUM dan COUNT ngitung nilai yang sama berkali-kali, jadi angka laporan bisa naik berlipat. Cara paling cepat ngedeteksinya adalah ngebandingin COUNT(*) sama COUNT(DISTINCT id_tabel_kiri) setelah JOIN dijalanin.
JOIN melipatgandakan baris kalau satu baris di tabel kiri cocok sama lebih dari satu baris di tabel kanan.
Nggak ada pesan error. Query-nya jalan, hasilnya keluar, dan angkanya salah.
Ini cerita satu laporan omzet yang naik 2,6 kali lipat gara-gara satu baris JOIN, plus query yang bisa kamu tempel buat ngecek query kamu sendiri.
Kejadiannya: omzet Agustus naik sendiri
Laporan omzet toko_berkah bulan Agustus 2025 keluar di angka Rp 612.400.000, dari 4.820 transaksi. Angka itu cocok sama rekap kasir.
Minggu berikutnya ada permintaan tambahan: laporan yang sama, tapi ditambahin kategori produk. Kolom kategori adanya di tabel transaksi_item, jadi query-nya ditambahin satu JOIN.
SELECT SUM(t.total) AS omzet
FROM transaksi t
JOIN transaksi_item ti ON ti.transaksi_id = t.id;
Hasilnya Rp 1.592.240.000.
Naik 160 persen dalam seminggu, tanpa ada transaksi baru. Angkanya sempat kepakai di rapat sebelum ada yang ngecek.
Penyebabnya kelihatan begitu jumlah barisnya dihitung. Tabel transaksi_item punya 12.532 baris untuk 4.820 transaksi, atau rata-rata 2,6 item per transaksi.
Karena kolom total nyimpen nilai per transaksi, satu transaksi senilai Rp 150.000 yang isinya 3 barang bakal dijumlahin tiga kali. Dan 612.400.000 dikali 2,6 hasilnya persis 1.592.240.000.
Kenapa JOIN bisa bikin baris kelipatan?
JOIN nyocokin tiap baris tabel kiri sama semua baris tabel kanan yang memenuhi syarat. Kalau satu baris kiri cocok sama tiga baris kanan, baris kiri itu muncul tiga kali di hasil. Nilai kolomnya ikut kesalin tiga kali juga, dan fungsi agregasi ngitung salinan itu sebagai data terpisah.
| Penyebab | Contoh | Seberapa sering |
|---|---|---|
| Relasi satu ke banyak | Satu transaksi punya banyak item | Paling sering |
| Kunci join nggak unik | Tabel master produk punya kode dobel | Sering |
| Tabel berversi tanpa filter tanggal | Riwayat harga produk per periode | Sedang |
| Dua JOIN satu ke banyak berturut-turut | Transaksi ke item, lalu item ke pengiriman | Sedang |
| Syarat ON kelupaan | Kelewat satu kolom di kunci gabungan | Jarang tapi fatal |
Baris keempat yang paling ganas. Kalau tabel A cocok sama 3 baris di B, dan tiap baris B cocok sama 2 baris di C, hasilnya 6 baris. Efeknya berlipat, bukan bertambah.
Baris kelima ngasilin cross join terselubung. Sepuluh ribu baris dikali sepuluh ribu baris jadi seratus juta baris, dan biasanya query-nya keburu dimatiin sebelum selesai.
Gimana cara ngedeteksi JOIN yang bikin baris kelipatan?
Cara paling cepat: bandingin jumlah baris hasil sama jumlah id unik tabel kiri. Kalau dua angka itu sama, JOIN kamu aman. Kalau baris hasilnya lebih besar, kamu lagi ngitung nilai yang sama berkali-kali. Pengecekan ini cuma butuh satu query dan berlaku buat JOIN mana pun.
SELECT
COUNT(*) AS baris_hasil,
COUNT(DISTINCT t.id) AS transaksi_unik
FROM transaksi t
JOIN transaksi_item ti ON ti.transaksi_id = t.id;
-- baris_hasil = 12.532
-- transaksi_unik = 4.820
-- faktor kelipatan = 2,6
Kalau ketahuan ada kelipatan, langkah berikutnya cari kunci mana yang dobel:
SELECT transaksi_id, COUNT(*) AS jumlah
FROM transaksi_item
GROUP BY transaksi_id
HAVING COUNT(*) > 1
ORDER BY jumlah DESC
LIMIT 20;
Query ini juga kepakai buat ngecek tabel master. Kalau tabel produk punya kode yang muncul dua kali, itu masalah kualitas data, bukan masalah query.
Kebiasaan yang murah dan ngehemat banyak waktu: tiap nambah satu JOIN, jalanin SELECT COUNT(*) sebelum dan sesudahnya. Kalau angkanya berubah dan kamu nggak nyangka, berhenti dulu.
Kenapa DISTINCT bukan solusinya?
Reaksi pertama kebanyakan orang adalah nambahin DISTINCT, dan di kasus tertentu angkanya emang balik bener. Masalahnya, DISTINCT nutupin gejala tanpa nyentuh penyebabnya, dan dia juga bakal ngebuang baris yang emang seharusnya kembar.
-- Berbahaya: dua transaksi berbeda dengan nilai sama ikut kebuang
SELECT DISTINCT t.id, t.total
FROM transaksi t
JOIN transaksi_item ti ON ti.transaksi_id = t.id;
Contoh nyatanya: dua pelanggan beda yang sama-sama belanja Rp 25.000 di menit yang sama. Kalau kolom id-nya nggak ikut dipilih, DISTINCT bakal nganggep dua baris itu satu.
DISTINCT juga mahal. Database harus ngurutin atau nge-hash seluruh hasil sebelum ngebuang duplikat, dan di tabel besar bedanya kerasa.
Cara benerin JOIN yang bikin baris kelipatan
- Agregasi dulu, baru JOIN. Ini perbaikan paling bener buat relasi satu ke banyak.
Setelah dikelompokin, tiap transaksi cuma punya satu baris pasangan. Cara kerja fungsi pengelompokannya ada di glossary aggregate function.WITH item_rekap AS ( SELECT transaksi_id, COUNT(*) AS jumlah_item FROM transaksi_item GROUP BY transaksi_id ) SELECT SUM(t.total) AS omzet FROM transaksi t LEFT JOIN item_rekap i ON i.transaksi_id = t.id; - Pakai EXISTS kalau kamu cuma mau nyaring. Kalau tabel kanan nggak dipakai kolomnya, jangan di-JOIN sama sekali.
SELECT SUM(t.total) FROM transaksi t WHERE EXISTS ( SELECT 1 FROM transaksi_item ti WHERE ti.transaksi_id = t.id ); - Lengkapi kunci gabungannya. Kalau tabel kanan unik berdasarkan dua kolom, tulis dua-duanya di ON. Kunci setengah selalu ngasilin kelipatan.
- Kasih filter periode buat tabel berversi. Tabel riwayat harga butuh syarat tanggal berlaku, kalau nggak satu produk bawa semua versi harganya.
ON h.produk_id = p.id AND t.tanggal BETWEEN h.berlaku_mulai AND h.berlaku_sampai - Pisahin agregasi yang beda butiran. Kalau kamu butuh jumlah item sekaligus jumlah pengiriman, bikin dua CTE terpisah lalu JOIN dua-duanya ke tabel utama. Nge-JOIN berantai bikin angkanya berlipat.
- Pasang uji jumlah baris di pipeline. Satu baris pengecekan yang gagal kalau
COUNT(*)lebih besar dariCOUNT(DISTINCT id)bakal nangkep masalah ini sebelum laporannya kekirim.
Hasil setelah diperbaiki: omzet balik ke Rp 612.400.000, dan kolom jumlah item tetap bisa ditampilin.
Kesalahan umum yang bikin masalah ini balik lagi
1. Ngandelin nama kolom buat nebak keunikan. Kolom bernama kode_produk belum tentu unik. Cek pakai GROUP BY dan HAVING.
2. Nambah JOIN di query yang udah panjang tanpa ngecek jumlah baris. Di query 80 baris, satu JOIN tambahan gampang lolos review.
3. Ngira LEFT JOIN lebih aman. LEFT JOIN tetap ngasilin kelipatan kalau tabel kanan punya pasangan lebih dari satu. Yang beda cuma perlakuan buat baris tanpa pasangan.
4. Ngecek total doang tanpa ngecek jumlah baris. Kalau kelipatannya nggak rata, misalnya cuma sebagian transaksi yang punya banyak item, totalnya bisa naik sedikit dan kelihatan masuk akal.
5. Ngabaikan kolom NULL di kunci join. Baris dengan kunci kosong justru ilang, bukan kelipatan. Dua masalah ini sering muncul bareng, dan yang satunya dibahas di semantik NULL di SQL.
FAQ
Gimana cara tau JOIN aku bikin baris dobel?
Jalanin satu query yang ngitung COUNT(*) dan COUNT(DISTINCT id) dari tabel kiri setelah JOIN. Kalau dua angkanya sama, JOIN kamu aman. Kalau COUNT(*) lebih besar, hasil bagi dua angka itu ngasih faktor kelipatannya. Cara ini jalan buat semua jenis JOIN dan cuma butuh beberapa detik, jadi layak dijadiin kebiasaan tiap kamu nambah tabel.
Apakah LEFT JOIN lebih aman dari INNER JOIN?
Nggak buat masalah ini. LEFT JOIN dan INNER JOIN sama-sama ngasilin baris kelipatan kalau tabel kanan punya lebih dari satu pasangan. Bedanya cuma di baris yang nggak punya pasangan sama sekali: LEFT JOIN tetap nyimpen barisnya dengan kolom kanan berisi NULL, sementara INNER JOIN ngebuangnya. Buat urusan kelipatan, dua-duanya sama berisikonya.
Kapan DISTINCT boleh dipakai?
Pakai DISTINCT kalau kamu emang butuh daftar nilai unik, misalnya daftar kota yang pernah ada transaksinya. Jangan pakai DISTINCT buat mbenerin angka yang kelipatan gara-gara JOIN, soalnya kamu cuma nutupin gejalanya dan berisiko ngebuang baris yang sah kembar. Perbaikan yang bener adalah ngagregasi tabel kanan sebelum di-JOIN.
Kenapa jumlah barisnya nambah padahal aku pakai INNER JOIN?
Karena INNER JOIN nggak batasin jumlah pasangan, dia cuma ngebuang baris yang nggak punya pasangan sama sekali. Satu baris kiri yang cocok sama lima baris kanan bakal muncul lima kali. Kalau kamu nyangka hasilnya bakal berkurang, itu asumsi yang keliru. INNER JOIN bisa ngasilin baris lebih banyak dari tabel asalnya.
Gimana cara nyegah masalah ini di pipeline data?
Tambahin uji jumlah baris di tiap tahap transformasi. Bentuk paling sederhananya satu query yang gagal kalau COUNT(*) lebih besar dari COUNT(DISTINCT kunci utama). Banyak tool transformasi modern punya uji keunikan bawaan yang tinggal kamu aktifin per kolom. Biaya masangnya lima menit, dan itu jauh lebih murah daripada nyari selisih angka di laporan yang udah kekirim.
Penutup
Dua hal yang layak kamu bawa. Pertama, JOIN yang bikin baris kelipatan nggak pernah ngasih error, jadi kamu yang harus ngecek. Kedua, perbaikannya bukan DISTINCT, tapi ngagregasi tabel kanan sebelum di-JOIN.
Buka query laporan yang paling sering kamu pakai, tambahin COUNT(*) dan COUNT(DISTINCT id) di atasnya, dan lihat dua angkanya sama atau nggak.
Buat jebakan JOIN yang satunya lagi, yaitu baris yang justru ilang, lanjut ke semantik NULL di SQL. Referensi resmi soal perilaku JOIN ada di dokumentasi PostgreSQL soal join.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Artikel terkait
Cara Membuat Pivot Dinamis di SQL (Panduan 2026)
Pivot dinamis di SQL ngubah baris jadi kolom tanpa kamu hardcode nama kolomnya. Ini cara bikinnya pakai CASE WHEN dan versi yang kolomnya ngikut data.
Menghitung YTD, QTD, dan MTD di SQL (Panduan 2026)
YTD, QTD, dan MTD ngukur total dari awal tahun, kuartal, atau bulan sampai hari ini. Ini cara ngitungnya di SQL pakai DATE_TRUNC, plus satu query gabungan.
Gap and Island Analysis di SQL
Gap and island analysis di SQL nyari deret data yang berturut-turut (island) dan celah yang bolong (gap). Cocok buat hitung streak login atau cari tanggal transaksi yang hilang.