Semi Join dan Anti Join di SQL: Filter Pakai Tabel Lain
TL;DR
Semi join di SQL ngambil baris dari tabel kiri yang punya minimal satu pasangan di tabel kanan, biasanya ditulis pakai EXISTS atau IN. Anti join kebalikannya, ngambil baris yang nggak punya pasangan sama sekali, ditulis pakai NOT EXISTS atau LEFT JOIN dengan cek NULL. Keduanya cuma filter baris tabel kiri dan nggak nambahin kolom dari tabel kanan.
Semi join di SQL ngambil baris dari tabel kiri yang punya pasangan di tabel kanan, tapi nggak nambahin kolom apa pun dari tabel kanan. Anti join kebalikannya, ngambil baris yang nggak punya pasangan sama sekali.
Dua istilah ini jarang muncul di menu SQL. Nggak ada keyword SEMI JOIN yang bisa kamu ketik langsung. Tapi polanya kepake tiap hari, cuma ditulis pakai EXISTS, IN, atau NOT EXISTS.
Begitu ngerti bedanya, banyak query filter yang tadinya ribet jadi jelas. Aku bahas keduanya di sini pakai contoh data toko biar kebayang.
Apa Itu Semi Join di SQL?
Semi join adalah operasi yang ngambil baris dari tabel kiri kalau ada minimal satu baris cocok di tabel kanan. Kata kuncinya: cukup satu kecocokan, dan baris kiri keluar sekali aja. Nggak ada penggandaan, nggak ada kolom tambahan dari tabel kanan.
Cara paling umum nulisnya pakai EXISTS:
SELECT p.pelanggan_id, p.nama
FROM pelanggan p
WHERE EXISTS (
SELECT 1 FROM transaksi t
WHERE t.pelanggan_id = p.pelanggan_id
);
Query ini jawab pertanyaan: "pelanggan mana aja yang pernah transaksi minimal sekali?" Kalau satu pelanggan punya 50 transaksi, dia tetap muncul sekali. Itu inti semi join.
Butuh penyegaran soal cara kerja subquery dan EXISTS? Cek dulu glossary subquery.
Apa Itu Anti Join di SQL?
Anti join ngambil baris dari tabel kiri yang nggak punya pasangan sama sekali di tabel kanan. Ini kebalikan persis dari semi join. Cara amannya pakai NOT EXISTS:
SELECT p.pelanggan_id, p.nama
FROM pelanggan p
WHERE NOT EXISTS (
SELECT 1 FROM transaksi t
WHERE t.pelanggan_id = p.pelanggan_id
);
Query ini jawab: "pelanggan mana yang belum pernah transaksi?" Berguna banget buat cari akun tidur, produk yang belum pernah kejual, atau data master yang nggak kepakai.
Anti join juga bisa ditulis pakai LEFT JOIN, aku tunjukin di bawah.
Semi Join Pakai IN, Apa Bedanya sama EXISTS?
Semi join juga bisa ditulis pakai IN. Hasilnya sama kayak EXISTS buat kasus normal. Bedanya kerasa waktu ada NULL dan waktu subquery balikin banyak baris.
-- Versi IN
SELECT p.pelanggan_id, p.nama
FROM pelanggan p
WHERE p.pelanggan_id IN (
SELECT t.pelanggan_id FROM transaksi t
);
Buat semi join, IN aman dan gampang dibaca. Yang bahaya itu versi negatifnya, NOT IN, buat anti join. Ini tabel bandinganya:
| Pola | Fungsi | Aman terhadap NULL? |
|---|---|---|
| EXISTS | Semi join | Aman |
| IN | Semi join | Aman |
| NOT EXISTS | Anti join | Aman |
| NOT IN | Anti join | Bahaya kalau ada NULL |
Aturan amanku: EXISTS dan NOT EXISTS buat semua kasus. Konsisten dan nggak kena jebakan NULL.
Kenapa NOT IN Bisa Bikin Query Balik Kosong?
Kalau daftar pembanding di NOT IN mengandung satu aja nilai NULL, seluruh hasil bisa jadi kosong. Ini karena NOT IN bandingin nilai satu per satu, dan ketemu NULL bikin hasilnya UNKNOWN, bukan TRUE.
Contoh jebakannya:
-- BAHAYA kalau kolom pelanggan_id di transaksi ada yang NULL
SELECT p.nama
FROM pelanggan p
WHERE p.pelanggan_id NOT IN (
SELECT t.pelanggan_id FROM transaksi t
);
Kalau ada satu transaksi dengan pelanggan_id NULL, query di atas balik kosong total. Padahal jelas ada pelanggan yang belum transaksi.
Makanya buat anti join, pakai NOT EXISTS. Dia ngecek keberadaan baris, bukan bandingin nilai, jadi NULL nggak ngerusak hasil.
Anti Join Pakai LEFT JOIN dan IS NULL
Cara lain bikin anti join: LEFT JOIN ke tabel kanan, terus ambil baris yang kolom kanannya NULL. Baris yang nggak ketemu pasangan otomatis dapat NULL di sisi kanan.
SELECT p.pelanggan_id, p.nama
FROM pelanggan p
LEFT JOIN transaksi t
ON t.pelanggan_id = p.pelanggan_id
WHERE t.pelanggan_id IS NULL;
Hasilnya sama persis kayak NOT EXISTS. Yang penting: kondisi join taruh di ON, dan cek NULL taruh di WHERE. Kalau kebalik, hasilnya bisa ngaco.
Pengin lebih paham beda menaruh kondisi di ON versus WHERE? Aku bahas di klausa USING vs ON pada JOIN.
Contoh Kasus: Pelanggan Tidur di toko_berkah
Di database toko_berkah, ada 1.240 pelanggan terdaftar. Aku mau cari yang belum pernah belanja sama sekali dalam 6 bulan terakhir, biar bisa dikirimin promo.
SELECT p.pelanggan_id, p.nama, p.kota
FROM pelanggan p
WHERE NOT EXISTS (
SELECT 1 FROM transaksi t
WHERE t.pelanggan_id = p.pelanggan_id
AND t.tanggal >= '2026-05-01'
);
Hasilnya 318 pelanggan tidur. Artinya 25,6% dari total pelanggan terdaftar nggak transaksi selama setengah tahun. Angka ini bikin tim toko sadar database pelanggannya banyak yang nganggur.
Kalau aku balik jadi semi join (EXISTS), hasilnya 922 pelanggan aktif. Dua angka ini dari data yang sama, cuma beda arah filter. Itulah gunanya paham semi versus anti join.
Kesalahan Umum
- Pakai NOT IN dengan kolom yang bisa NULL. Ganti ke NOT EXISTS. Ini bug paling sering di anti join.
- Naruh kondisi NULL di ON, bukan WHERE. Di anti join LEFT JOIN, cek IS NULL harus di WHERE.
- Pakai inner join buat filter. Kalau cuma mau cek keberadaan, inner join bisa gandain baris. Pakai semi join.
- SELECT kolom dari tabel kanan di semi join. Semi join nggak nyediain kolom tabel kanan. Kalau butuh kolomnya, kamu sebenernya butuh JOIN biasa.
FAQ
Pertanyaan yang sering muncul soal semi join dan anti join di SQL.
Penutup
Semi join ngambil yang punya pasangan, anti join ngambil yang nggak punya. Keduanya filter baris tabel kiri, bukan gabung kolom.
Buat aman, pakai EXISTS buat semi join dan NOT EXISTS buat anti join. Hindari NOT IN kalau ada kemungkinan NULL.
Coba tulis query "pelanggan yang belum pernah beli" di databasemu sendiri pakai NOT EXISTS. Bandingin sama versi NOT IN dan lihat bedanya kalau ada NULL.
Mau lanjut? Baca USING vs ON pada JOIN SQL. Detail soal EXISTS ada di dokumentasi resmi PostgreSQL.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Artikel terkait
Crosstab (Pivot) di PostgreSQL: Ubah Baris Jadi Kolom
Crosstab di PostgreSQL mivot data dari format panjang (baris) jadi format lebar (kolom), mirip pivot table Excel. Butuh extension tablefunc, dan ada alternatif pakai CASE atau FILTER kalau kolomnya tetap.
INSERT ON CONFLICT di PostgreSQL
INSERT ON CONFLICT bikin PostgreSQL insert baris baru atau update yang udah ada dalam satu perintah, tanpa error gara-gara duplikat primary key.
7 Antipattern SQL yang Bikin Query Lambat
Tujuh pola SQL yang diam-diam bikin query lambat, dari SELECT bintang, fungsi di kolom filter, sampai OFFSET besar. Plus cara benerin tiap satunya.