TL;DR
EXISTS adalah operator SQL yang mengecek apakah sebuah subquery menghasilkan minimal satu baris, lalu mengembalikan TRUE atau FALSE. Begitu nemu satu baris yang cocok, dia langsung berhenti nyari, jadi biasanya lebih cepat dari IN di tabel besar. EXISTS juga aman dari jebakan NULL yang bikin NOT IN diam-diam ngasih hasil kosong. Isi SELECT di dalam EXISTS gak pernah dipakai, makanya orang biasa nulis SELECT 1 aja.
EXISTS ngecek satu hal doang: ada gak baris yang cocok? Begitu ketemu satu, dia langsung berhenti nyari.
IN gak gitu. Dia ngumpulin dulu semua nilai dari subquery, baru dibandingin satu per satu.
Di tabel 100 baris, selisihnya gak kerasa. Di tabel 5 juta baris, ini bedanya antara query 0,3 detik sama query 40 detik.
EXISTS adalah operator SQL yang mengecek apakah sebuah subquery menghasilkan minimal satu baris. Hasilnya cuma dua: TRUE kalau ada, FALSE kalau kosong.
SELECT kolom
FROM tabel_a a
WHERE EXISTS (
SELECT 1
FROM tabel_b b
WHERE b.a_id = a.id
);
Perhatiin SELECT 1 di dalamnya. Itu bukan typo.
Soalnya isi subquery-nya gak pernah dipakai. EXISTS cuma peduli: ada barisnya atau nggak.
Kamu bisa nulis SELECT *, SELECT nama, atau bahkan SELECT 'terserah', hasilnya identik. SELECT 1 udah jadi konvensi karena paling pendek dan langsung ngasih sinyal ke pembaca bahwa isinya gak penting.
Pakai dataset toko_berkah. Bikin tabelnya:
CREATE TABLE pelanggan (
pelanggan_id INT PRIMARY KEY,
nama VARCHAR(100) NOT NULL,
kota VARCHAR(50)
);
CREATE TABLE pesanan (
pesanan_id INT PRIMARY KEY,
pelanggan_id INT,
tanggal DATE,
total DECIMAL(12,2)
);
Isi datanya:
INSERT INTO pelanggan (pelanggan_id, nama, kota) VALUES
(1, 'Warung Bu Siti', 'Bandung'),
(2, 'Toko Pak Anwar', 'Cimahi'),
(3, 'Kios Mang Ujang', 'Soreang'),
(4, 'Warung Teh Nina', 'Bandung'),
(5, 'Toko Bu Rina', 'Cimahi');
INSERT INTO pesanan (pesanan_id, pelanggan_id, tanggal, total) VALUES
(101, 1, '2026-01-05', 1250000),
(102, 2, '2026-01-06', 890000),
(103, 1, '2026-01-09', 640000),
(104, NULL, '2026-01-11', 320000);
Perhatiin pesanan 104. pelanggan_id-nya NULL (data kotor dari sistem kasir yang gagal nyimpen ID).
Ingat NULL itu. Dia bakal jadi bom waktu sebentar lagi.
SELECT p.pelanggan_id, p.nama, p.kota
FROM pelanggan p
WHERE EXISTS (
SELECT 1
FROM pesanan o
WHERE o.pelanggan_id = p.pelanggan_id
);
Hasil: Warung Bu Siti, Toko Pak Anwar. Dua orang.
Perhatiin Bu Siti punya dua pesanan (101 dan 103), tapi cuma muncul sekali. EXISTS gak bikin baris berlipat. Dia berhenti begitu nemu yang pertama.
Coba pakai JOIN. Bu Siti bakal muncul dua kali, dan kamu harus nambahin DISTINCT buat benerin.
SELECT p.pelanggan_id, p.nama, p.kota
FROM pelanggan p
WHERE NOT EXISTS (
SELECT 1
FROM pesanan o
WHERE o.pelanggan_id = p.pelanggan_id
);
Hasil: Kios Mang Ujang, Warung Teh Nina, Toko Bu Rina. Tiga pelanggan.
Itu daftar orang yang perlu di-follow up tim sales besok pagi. Bener dan lengkap.
Sekarang coba pertanyaan yang sama pakai NOT IN:
SELECT p.pelanggan_id, p.nama
FROM pelanggan p
WHERE p.pelanggan_id NOT IN (
SELECT o.pelanggan_id FROM pesanan o
);
Hasilnya: 0 baris.
Bukan tiga. Nol. Padahal jelas-jelas ada tiga pelanggan yang belum pernah pesan.
Ingat pesanan 104 yang pelanggan_id-nya NULL?
Waktu SQL bandingin sebuah angka sama NULL, hasilnya bukan TRUE atau FALSE. Hasilnya UNKNOWN.
NOT IN butuh semua perbandingan bernilai TRUE buat ngeloloskan sebuah baris. Satu UNKNOWN aja, seluruh baris kebuang.
Jadi begitu ada satu NULL nyempil di subquery, NOT IN bakal ngasih hasil kosong. Tanpa error. Tanpa warning.
Ini penjelasan lengkapnya ada di artikel NULL di SQL, dan cara kerja three-valued logic-nya dibahas di dokumentasi subquery expressions PostgreSQL.
Pilihan pertama, pakai NOT EXISTS. Dia kebal NULL, dan itu alasan utama orang milih dia.
Pilihan kedua, kalau tetep mau NOT IN, saring NULL-nya dulu:
SELECT p.pelanggan_id, p.nama
FROM pelanggan p
WHERE p.pelanggan_id NOT IN (
SELECT o.pelanggan_id
FROM pesanan o
WHERE o.pelanggan_id IS NOT NULL
);
Sekarang keluar tiga baris. Tapi kamu harus inget nulis IS NOT NULL-nya tiap kali. NOT EXISTS gak nuntut kamu inget apa-apa.
| Situasi | Pakai | Alasan |
|---|---|---|
| Cek keberadaan, tabel besar | EXISTS | Berhenti di baris pertama yang cocok |
| Cek ketiadaan, subquery bisa punya NULL | NOT EXISTS | NOT IN bakal ngasih hasil kosong |
| Daftar nilai pendek dan pasti | IN | Lebih pendek, lebih gampang dibaca |
| Butuh kolom dari tabel kedua | JOIN | EXISTS gak bisa ngambil kolom |
| Butuh ngitung berapa banyak | JOIN + GROUP BY | EXISTS cuma jawab ada atau nggak |
Aturan yang aku pakai: kalau pertanyaannya "ada gak?", jawabannya EXISTS. Kalau pertanyaannya "berapa?" atau "apa aja isinya?", jawabannya JOIN.
Kamu bisa numpuk kondisi di dalam subquery-nya:
SELECT p.nama
FROM pelanggan p
WHERE EXISTS (
SELECT 1
FROM pesanan o
WHERE o.pelanggan_id = p.pelanggan_id
AND o.total > 1000000
AND o.tanggal >= DATE '2026-01-01'
);
Artinya: pelanggan yang punya minimal satu pesanan di atas 1 juta sejak awal 2026.
Hasilnya: Warung Bu Siti doang.
Ini pola yang susah ditiru pakai IN, soalnya IN cuma bisa bandingin satu kolom.
-- SALAH: gak ada korelasi
WHERE EXISTS (SELECT 1 FROM pesanan)
Ini TRUE buat semua baris, selama tabel pesanan gak kosong. Filternya gak ngefek sama sekali, dan kamu bakal bingung kenapa semua data keluar.
Selalu ada kondisi yang nyambungin kolom subquery sama kolom tabel luar.
Udah dibahas panjang di atas. Kalau kolomnya nullable, pakai NOT EXISTS. Titik.
-- Boros, gak perlu
WHERE EXISTS (SELECT COUNT(*) FROM pesanan o WHERE ...)
COUNT(*) maksa database ngitung semua baris yang cocok. EXISTS cuma butuh satu. Pakai SELECT 1.
EXISTS dijalanin sekali buat tiap baris tabel luar. Tanpa index di pesanan.pelanggan_id, tabel pesanan bakal di-scan penuh berkali-kali. Ini yang bikin orang salah nyalahin EXISTS sebagai fungsi yang lambat.
EXISTS ngecek ada atau nggaknya baris, dan langsung berhenti begitu nemu satu yang cocok. IN ngumpulin dulu semua nilai dari subquery, baru dibandingin satu per satu. Di tabel kecil selisihnya gak kerasa. Di tabel jutaan baris, EXISTS biasanya jauh lebih ngebut. EXISTS juga aman dari NULL.
Hampir pasti gara-gara ada NULL di subquery-nya. Waktu SQL bandingin sebuah nilai dengan NULL, hasilnya bukan TRUE atau FALSE, tapi UNKNOWN. NOT IN butuh semua perbandingan bernilai TRUE, dan UNKNOWN bikin itu gak pernah kejadian. Solusinya pakai NOT EXISTS, atau tambahin WHERE kolom IS NOT NULL di dalam subquery.
Karena isinya emang gak pernah dipakai. EXISTS cuma peduli apakah subquery-nya menghasilkan baris atau nggak, bukan apa isi barisnya. Kamu bisa nulis SELECT *, SELECT nama, atau bahkan SELECT 'apa aja' dan hasilnya sama persis. SELECT 1 udah jadi konvensi karena paling pendek.
Tergantung tujuannya. Kalau kamu cuma mau ngecek keberadaan data dan gak butuh kolom dari tabel kedua, EXISTS lebih tepat dan gak bikin baris berlipat. Kalau kamu butuh kolom dari kedua tabel, JOIN yang bener. Optimizer PostgreSQL dan SQL Server modern sering ngubah keduanya jadi rencana eksekusi yang mirip.
Bisa secara teknis, tapi hampir selalu salah. Kalau subquery-nya gak nyambung ke tabel luar, misalnya WHERE EXISTS (SELECT 1 FROM pesanan), hasilnya TRUE buat semua baris selama tabel pesanan gak kosong. Jadi filternya gak ngefek sama sekali. Selalu pastiin ada kondisi yang nyambungin kolom subquery dengan kolom tabel luar.
Tiga hal yang perlu diinget:
Semua query di atas siap copy-paste. Coba jalanin yang NOT IN dulu, liat hasilnya kosong, terus ganti jadi NOT EXISTS. Momen "oh iya ya" itu yang bikin nempel.
Latihan interaktifnya ada di halaman fungsi EXISTS. Konsep three-valued logic yang bikin NULL berbahaya aku jelasin di glosarium NULL.
Lanjut baca: Operator IN dan BETWEEN SQL (biar kamu tau kapan IN masih jadi pilihan yang paling tepat).
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Analisa per kuartal, hari kerja, atau musim jadi ribet kalau tiap query ngitung ulang atribut tanggal. Tabel kalender nyimpen semua atribut itu sekali, biar tinggal di-JOIN.
Laporan penjualan harian sering bolong di hari tanpa transaksi. Ini cara bikin deret tanggal lengkap di SQL biar tiap hari muncul, walau nilainya nol.
Struktur organisasi tersimpan sebagai kolom id_atasan yang saling nunjuk. Ini cara narik rantai jabatan, hitung total bawahan, dan span of control cuma pakai SQL.