Pertanyaan “siapa yang belum pernah memesan?” terdengar sederhana, tetapi jawabannya menuntut sesuatu yang belum pernah kita perlukan: kondisi yang hanya bertanya ada atau tidak ada, tanpa peduli isi barisnya. Operator EXISTS dibuat untuk persis kebutuhan ini — ia menilai apakah subquery menghasilkan setidaknya satu baris, lalu mengembalikan benar atau salah. Pada chapter ini kita membedah EXISTS dan lawannya NOT EXISTS, memahami mengapa isi SELECT di dalam subquery tidak pernah diperhatikan, mengapa EXISTS berhenti begitu menemukan baris pertama, lalu membuktikan secara nyata kenapa NOT EXISTS lebih aman daripada NOT IN ketika data bisa berisi NULL.
Menyiapkan Empat Tabel Latihan #
Struktur dan data sama dengan dua chapter sebelumnya, tetapi kali ini seluruh empat tabel kita perlukan karena pertanyaan keberadaan akan kita ajukan ke dua arah: pelanggan terhadap pesanan, dan produk terhadap detail transaksi:
DROP DATABASE IF EXISTS belajar_sub;
CREATE DATABASE belajar_sub;
USE belajar_sub;Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 1 row affected (0.00 sec)
Database changedCREATE TABLE produk (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(50) NOT NULL,
kategori VARCHAR(30) NOT NULL,
harga DECIMAL(10,2) NOT NULL
);
CREATE TABLE pelanggan (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(50) NOT NULL
);
CREATE TABLE pesanan (
id INT AUTO_INCREMENT PRIMARY KEY,
tanggal DATE NOT NULL,
pelanggan INT NULL
);
CREATE TABLE detail_pesanan (
pesanan_id INT NOT NULL,
produk_id INT NOT NULL,
jumlah INT NOT NULL
);
INSERT INTO produk (nama, kategori, harga) VALUES
('Kopi Gayo', 'kopi', 55000.00),
('Kopi Toraja', 'kopi', 62000.00),
('Teh Melati', 'teh', 25000.00),
('Teh Hijau', 'teh', 30000.00),
('Gula Aren', 'pemanis', 15000.00),
('Susu Kental', 'pemanis', 18000.00);
INSERT INTO pelanggan (nama) VALUES
('Budi'), ('Siti'), ('Rina'), ('Andi'), ('Dewi');
INSERT INTO pesanan (tanggal, pelanggan) VALUES
('2026-01-05', 1),
('2026-01-06', 2),
('2026-01-08', 1),
('2026-01-10', 3),
('2026-01-12', 2),
('2026-01-15', NULL);
INSERT INTO detail_pesanan (pesanan_id, produk_id, jumlah) VALUES
(1, 1, 2),
(1, 5, 1),
(2, 3, 3),
(3, 3, 1),
(4, 2, 2),
(4, 3, 2),
(5, 1, 1),
(5, 5, 4),
(6, 2, 1);Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
Query OK, 6 rows affected (0.00 sec)
Records: 6 Duplicates: 0 Warnings: 0
Query OK, 5 rows affected (0.00 sec)
Records: 5 Duplicates: 0 Warnings: 0
Query OK, 6 rows affected (0.00 sec)
Records: 6 Duplicates: 0 Warnings: 0
Query OK, 9 rows affected (0.00 sec)
Records: 9 Duplicates: 0 Warnings: 0Ingat dua fakta data yang akan sering kembali muncul: Andi dan Dewi belum pernah memesan, dan pesanan id 6 adalah pesanan tunai dengan pelanggan bernilai NULL. Tabelnya sendiri sudah kita periksa di chapter Subquery dalam WHERE, jadi kali ini kita langsung masuk ke operatornya.
EXISTS: Cukup Tahu Ada #
Query berikut menjawab “pelanggan mana yang punya setidaknya satu pesanan?”:
SELECT nama FROM pelanggan WHERE EXISTS (SELECT 1 FROM pesanan WHERE pesanan.pelanggan = pelanggan.id) ORDER BY nama;Output yang Dihasilkan:
+------+
| nama |
+------+
| Budi |
| Rina |
| Siti |
+------+
3 rows in set (0.00 sec)Budi, Rina, dan Siti lolos — ketiganya memang tercatat punya pesanan. Mari kita bedah cara kerjanya:
WHERE EXISTS (...)— kondisi ini tidak membandingkan nilai apa pun; ia hanya menunggu jawaban benar atau salah dari subquery.SELECT 1 FROM pesanan WHERE pesanan.pelanggan = pelanggan.id— subquery berkorelasi ini mencari baris pesanan yang milik pelanggan baris luar.SELECT 1adalah konvensi: isi daftarSELECTtidak pernah diperhatikan olehEXISTS, yang dinilai hanya ada atau tidaknya baris hasil.- Evaluasi per baris luar — untuk Budi (id 1), subquery menemukan pesanan 1 dan 3, jadi
EXISTSbernilai benar; untuk Andi (id 4), subquery tidak menemukan satu baris pun, jadiEXISTSbernilai salah.
Dua sifat EXISTS layak digarisbawahi. Pertama, isi SELECT di dalam subquery sungguh diabaikan — SELECT 1, SELECT *, maupun SELECT 'x' menghasilkan jawaban yang sama; angka 1 dipilih karena paling murah ditulis dan langsung memberi tahu pembaca bahwa kolomnya tidak bermakna. Kedua, EXISTS berhenti di baris pertama yang ditemukan: begitu satu baris pesanan Budi ketemu, sisanya tidak perlu diperiksa, karena jawaban benar sudah pasti. Pada tabel besar, sifat berhenti-dini ini membuat EXISTS jauh lebih murah daripada menghitung seluruh baris dengan COUNT(*) hanya untuk tahu ada atau tidak.
NOT EXISTS: Pelanggan yang Belum Pernah Pesan #
Membalik kondisinya memberi kita daftar yang belum punya pasangan. NOT EXISTS bernilai benar justru ketika subquery tidak menemukan satu baris pun:
SELECT nama FROM pelanggan WHERE NOT EXISTS (SELECT 1 FROM pesanan WHERE pesanan.pelanggan = pelanggan.id) ORDER BY nama;Output yang Dihasilkan:
+------+
| nama |
+------+
| Andi |
| Dewi |
+------+
2 rows in set (0.00 sec)Andi dan Dewi tampil — keduanya memang tanpa pesanan. Pola “cari yang belum punya pasangan” ini punya nama resmi di dunia database: anti-join. Kita pernah membangunnya dengan LEFT JOIN ... WHERE kolom_kanan IS NULL di chapter LEFT JOIN; NOT EXISTS adalah bentuk deklaratifnya — tidak perlu memikirkan arah join atau kolom mana yang menjadi NULL. Kedua bentuk menghasilkan jawaban yang sama; pilih bentuk yang paling mudah dibaca oleh tim yang merawat query tersebut.
Pola yang sama berlaku untuk pasangan tabel mana pun. Produk yang belum pernah masuk transaksi juga tinggal membalik arah pencarian ke detail_pesanan:
SELECT nama FROM produk WHERE NOT EXISTS (SELECT 1 FROM detail_pesanan WHERE detail_pesanan.produk_id = produk.id) ORDER BY nama;Output yang Dihasilkan:
+-------------+
| nama |
+-------------+
| Susu Kental |
| Teh Hijau |
+-------------+
2 rows in set (0.00 sec)Susu Kental dan Teh Hijau belum pernah dipesan sama sekali. Perhatikan bahwa subquery ini tetap berkorelasi — detail_pesanan.produk_id = produk.id mengikat pencarian ke baris produk luar — hanya tabel yang dijadikan acuan berganti.
NOT IN versus NOT EXISTS #
Di chapter Subquery dalam WHERE kita sudah menyaksikan NOT IN tumbang diam-diam karena satu NULL di hasil subquerynya. Sekarang kita adu keduanya bersebelahan pada data yang sama — daftar pelanggan tanpa pesanan, dengan pesanan tunai NULL tetap berada di tempatnya. Versi NOT IN lebih dulu:
SELECT id, nama FROM pelanggan WHERE id NOT IN (SELECT pelanggan FROM pesanan);Output yang Dihasilkan:
Empty set (0.00 sec)Hasilnya kosong — NULL dari pesanan tunai meracuni semua perbandingan, dan tidak ada error yang memberi tahu kita. Versi NOT EXISTS pada bagian sebelumnya menghadapi data yang persis sama dan tetap menghasilkan Andi dan Dewi dengan benar. Beda nasibnya berasal dari cara kerja keduanya: NOT IN membandingkan nilai pelanggan terhadap setiap anggota daftar hasil subquery (termasuk NULL, yang meracuni semua perbandingan), sedangkan NOT EXISTS hanya bertanya “apakah subquery menemukan baris yang cocok?” — NULL di baris lain tidak pernah ikut dibandingkan.
Membersihkan Database Latihan #
Rangkaian latihan selesai dan database latihan kita jatuhkan kembali:
DROP DATABASE belajar_sub;Output yang Dihasilkan:
Query OK, 4 rows affected (0.00 sec)Referensi #
- EXISTS and NOT EXISTS Subqueries — MySQL 8.4 Reference Manual
- Subqueries — MySQL 8.4 Reference Manual
- JOIN Clause — MySQL 8.4 Reference Manual
EXISTS memperkenalkan subquery yang benar-benar dievaluasi per baris luar dan melihat kolom tabel luar sebagai acuan. Pola per-baris itu bukan trik khusus EXISTS — subquery apa pun bisa merujuk kolom luar, dan perilakunya punya nama: correlated subquery. Chapter berikutnya membedahnya tuntas, dari contoh “produk di atas rata-rata kategorinya sendiri” hingga hitungannya yang membuat kita paham kapan pola ini menjadi mahal.