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.
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.
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.
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.
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.
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;
Setelah dikelompokin, tiap transaksi cuma punya satu baris pasangan. Cara kerja fungsi pengelompokannya ada di glossary aggregate function.SELECT SUM(t.total)
FROM transaksi t
WHERE EXISTS (
SELECT 1 FROM transaksi_item ti WHERE ti.transaksi_id = t.id
);ON h.produk_id = p.id
AND t.tanggal BETWEEN h.berlaku_mulai AND h.berlaku_sampaiCOUNT(*) lebih besar dari COUNT(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.
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.
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.
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.
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.
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.
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.
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.
Kolom waktu isinya angka gede kayak 1704067200 dan bikin bingung? Itu Unix timestamp. Ini cara ngubahnya jadi tanggal beneran di SQL, plus balik lagi.
Data transaksi tersimpan UTC, tapi laporan harus jam WIB. Kalau salah konversi, angka penjualan tengah malam bisa kecatat di tanggal yang salah. Ini cara handle timezone di SQL dengan benar.
Nambah 30 hari ke tanggal invoice, ngurangin sebulan buat cari periode lalu, ngitung selisih hari antar order. Semua itu aritmetika tanggal, dan SQL punya operator INTERVAL buat ngerjainnya.