I35๐JOINโ
โโDasarTulis Query
Pelanggan yang Belum Pernah Order
#anti-join#left-join#not-exists
Pertanyaan
"Tim marketing mau kirim voucher ke pelanggan yang sudah daftar tapi belum pernah order sama sekali. Tampilkan daftar pelanggan tersebut."
Tabel pelanggan:
| pelanggan_id | nama | kota |
|---|---|---|
| 1 | Budi | Jakarta |
| 2 | Siti | Bandung |
| 3 | Andi | Surabaya |
| 4 | Dewi | Jakarta |
| 5 | Rina | Medan |
Tabel pesanan:
| pesanan_id | pelanggan_id | tanggal | total |
|---|---|---|---|
| 101 | 1 | 2026-09-01 | 150000 |
| 102 | 3 | 2026-09-02 | 85000 |
| 103 | 1 | 2026-09-05 | 60000 |
| 104 | 3 | 2026-09-07 | 120000 |
Jawaban Singkat
Ini pola anti-join. Ambil semua pelanggan dengan LEFT JOIN ke pesanan, lalu sisakan baris yang sisi pesanannya NULL (WHERE o.pesanan_id IS NULL). Alternatif yang sama benarnya adalah NOT EXISTS. Saya menghindari NOT IN karena bisa mengembalikan hasil kosong kalau subquery-nya mengandung NULL.
Pembahasan
- Mulai dari tabel yang mau ditampilkan. Yang dicari pelanggan, jadi
pelanggandi sisi kiri. - LEFT JOIN ke pesanan. Pelanggan yang punya pesanan akan terisi kolom pesanannya. Yang tidak punya pesanan tetap muncul dengan kolom pesanan NULL.
- Saring yang NULL. Cek kolom yang pasti tidak NULL kalau barisnya ada, idealnya primary key tabel kanan (
o.pesanan_id). Jangan cek kolom yang memang bisa NULL secara alami, misalnya kolom catatan. - Tidak perlu
DISTINCT. Baris yang lolos filter pasti tidak punya pasangan, jadi tidak mungkin berlipat.
Query
SELECT c.pelanggan_id, c.nama, c.kota FROM pelanggan AS c LEFT JOIN pesanan AS o ON o.pelanggan_id = c.pelanggan_id WHERE o.pesanan_id IS NULL ORDER BY c.pelanggan_id;
Versi NOT EXISTS, hasilnya sama:
SELECT c.pelanggan_id, c.nama, c.kota FROM pelanggan AS c WHERE NOT EXISTS ( SELECT 1 FROM pesanan AS o WHERE o.pelanggan_id = c.pelanggan_id ) ORDER BY c.pelanggan_id;
Contoh Output
| pelanggan_id | nama | kota |
|---|---|---|
| 2 | Siti | Bandung |
| 4 | Dewi | Jakarta |
| 5 | Rina | Medan |
Jebakan Umum
- Pakai
INNER JOINlaluWHERE o.pesanan_id IS NULL. Hasilnya selalu kosong, karena INNER JOIN sudah membuang pelanggan tanpa pesanan. - Pakai
NOT IN (SELECT pelanggan_id FROM pesanan)padahalpesanan.pelanggan_idbisa NULL (misalnya pesanan tamu). Satu NULL saja membuat seluruh hasil kosong. - Mengecek
IS NULLpada kolom yang bisa NULL secara alami di tabel kanan, sehingga pelanggan yang sebenarnya punya pesanan ikut lolos. - Menaruh filter tambahan untuk pesanan (misalnya periode) di
WHERE, bukan diON. Untuk anti-join berperiode, kondisi tanggal harus ikut diONatau di dalam subqueryNOT EXISTS.
Follow-up dari Interviewer
- "Kalau yang dicari pelanggan yang tidak order di bulan September saja?" Pindahkan filter tanggal ke dalam kondisi join:
ON o.pelanggan_id = c.pelanggan_id AND o.tanggal >= DATE '2026-09-01' AND o.tanggal < DATE '2026-10-01', lalu tetapWHERE o.pesanan_id IS NULL. - "Mana yang lebih cepat, LEFT JOIN IS NULL atau NOT EXISTS?" Di engine modern biasanya diubah jadi rencana anti-join yang sama.
NOT EXISTSsering lebih jelas niatnya dan aman dari NULL. - "Bisa pakai EXCEPT?" Bisa untuk mengambil id-nya:
SELECT pelanggan_id FROM pelanggan EXCEPT SELECT pelanggan_id FROM pesanan, lalu join balik kalau butuh kolom lain.
Yang Sebenarnya Diuji
Pemahaman bahwa LEFT JOIN menghasilkan NULL untuk baris tanpa pasangan, dan bagaimana memanfaatkannya sebagai anti-join. Interviewer juga sering memancing soal jebakan NOT IN dengan NULL.
Catatan Dialect
- Kedua versi (LEFT JOIN dan NOT EXISTS) jalan di DuckDB, PostgreSQL, MySQL, dan BigQuery.
- DuckDB juga punya sintaks eksplisit
ANTI JOIN:SELECT * FROM pelanggan AS c ANTI JOIN pesanan AS o ON o.pelanggan_id = c.pelanggan_id.