Menyaring terhadap satu nilai sudah lancar — kategori = 'kertas' — tetapi kebutuhan nyata sering berbentuk daftar: “produk dari kategori kertas, elektronik, atau alat tulis”, “pelanggan dari Jakarta, Bandung, dan Surabaya”. Menulis rantai OR untuk setiap anggota daftar itu merepotkan dan rawan salah. Operator IN menyederhanakannya menjadi satu perbandingan terhadap daftar nilai, dan NOT IN membaliknya menjadi “di luar daftar”. Di balik kesederhanaan itu, NOT IN menyimpan satu jebakan NULL paling terkenal di dunia SQL: satu nilai NULL di dalam daftar membuat query mengembalikan nol baris — tanpa error, tanpa peringatan. Pada chapter ini kita memakai keduanya, membedah jebakannya sampai ke mekanismenya, lalu menutup dengan cara memperbaikinya.
Menyiapkan Tabel Latihan #
Tabel produk Toko Maju Jaya yang sama dipakai lagi — delapan baris tetap dengan tiga kategori. Tabel kecil daftar_hemat akan menyusul di bagian jebakan NULL, jadi untuk sekarang cukup produk saja:
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
kategori text NOT NULL,
harga numeric(10, 2) NOT NULL,
stok integer NOT NULL,
aktif boolean NOT NULL,
tanggal_masuk date,
promo text
);
INSERT INTO produk (id, nama, kategori, harga, stok, aktif, tanggal_masuk, promo) VALUES
(1, 'Buku Tulis 58', 'alat tulis', 4500, 120, true, DATE '2025-11-10', NULL),
(2, 'Pulpen Hitam', 'alat tulis', 3000, 200, true, DATE '2025-12-01', 'Diskon 10%'),
(3, 'Kertas HVS A4', 'kertas', 55000, 40, true, DATE '2026-01-05', 'Diskon 50%'),
(4, 'Stapler Besar', 'alat tulis', 24000, 15, false, DATE '2026-01-20', NULL),
(5, 'Amplop Coklat', 'kertas', 1500, 500, true, DATE '2026-02-14', NULL),
(6, 'Penghapus', 'alat tulis', 2000, 0, false, DATE '2026-02-14', NULL),
(7, 'Kalkulator Solar', 'elektronik', 185000, 8, true, DATE '2026-03-02', 'Diskon 25%'),
(8, 'Penggaris 30cm', 'alat tulis', 3500, 85, true, DATE '2026-03-15', 'Diskon 5%');Output yang Dihasilkan:
CREATE TABLE
INSERT 0 8IN dengan Daftar Literal #
Bentuk dasar IN membandingkan satu ekspresi dengan daftar nilai literal di dalam kurung, dipisah koma. Baris lolos bila nilainya cocok dengan salah satu anggota daftar — angka, teks, tanggal, semua boleh sepanjang tipenya sejenis dengan kolom yang dibandingkan:
SELECT id, nama, kategori FROM produk
WHERE kategori IN ('kertas', 'elektronik')
ORDER BY id;
SELECT id, nama FROM produk
WHERE nama IN ('Penghapus', 'Pulpen Hitam', 'Kalkulator Solar')
ORDER BY id;
SELECT id, nama, kategori FROM produk
WHERE kategori NOT IN ('kertas', 'elektronik')
ORDER BY id;Output yang Dihasilkan:
id | nama | kategori
----+------------------+------------
3 | Kertas HVS A4 | kertas
5 | Amplop Coklat | kertas
7 | Kalkulator Solar | elektronik
(3 rows)
id | nama
----+------------------
2 | Pulpen Hitam
6 | Penghapus
7 | Kalkulator Solar
(3 rows)
id | nama | kategori
----+----------------+------------
1 | Buku Tulis 58 | alat tulis
2 | Pulpen Hitam | alat tulis
4 | Stapler Besar | alat tulis
6 | Penghapus | alat tulis
8 | Penggaris 30cm | alat tulis
(5 rows)Tiga hasil itu memperlihatkan pola dasarnya:
kategori IN ('kertas', 'elektronik')menjaring tiga produk dari dua kategori tersebut — hasil yang sama dengankategori = 'kertas' OR kategori = 'elektronik'dari chapter AND OR NOT, tetapi ditulis dalam satu baris.nama IN (...)bekerja pada kolom teks apa pun — di sini tiga produk spesifik dijaring lewat daftar namanya, terlepas dari kategorinya.kategori NOT IN ('kertas', 'elektronik')membalik logikanya: lima produk di luar kedua kategori itu lolos. Selama semua anggota daftar bukan NULL,NOT INberperilaku persis seperti intuisi kita.
IN versus OR: Dua Bentuk, Satu Makna #
Chapter sebelumnya sudah menyebutnya; sekarang kita buktikan langsung. Dua query berikut memakai bentuk berbeda untuk pertanyaan yang sama — “produk dengan harga tepat 4500 atau 55000”:
SELECT id, nama, harga FROM produk
WHERE harga = 4500 OR harga = 55000
ORDER BY id;
SELECT id, nama, harga FROM produk
WHERE harga IN (4500, 55000)
ORDER BY id;Output yang Dihasilkan:
id | nama | harga
----+---------------+----------
1 | Buku Tulis 58 | 4500.00
3 | Kertas HVS A4 | 55000.00
(2 rows)
id | nama | harga
----+---------------+----------
1 | Buku Tulis 58 | 4500.00
3 | Kertas HVS A4 | 55000.00
(2 rows)Hasilnya identik: Buku Tulis 58 dan Kertas HVS A4. Planner PostgreSQL bahkan menganggap keduanya sama di tahap optimasi — rantai OR kesetaraan dan IN ditulis ulang ke bentuk internal yang sama, sehingga tidak ada alasan performa untuk memilih salah satu. Pemilihannya murni keterbacaan: untuk tiga nilai ke atas, IN menang jauh; untuk negasi (NOT), IN nyaris tanpa saingan karena NOT (a = 1 OR a = 2 OR a = 3) jauh lebih mudah salah baca daripada a NOT IN (1, 2, 3).
Daftar Kosong: Kesalahan Sintaks dan Subquery Kosong #
Dua kasus tepi “daftar tanpa anggota” berperilaku sangat berbeda. Pertama, daftar literal yang benar-benar kosong — IN () — bukan query yang mengembalikan nol baris, melainkan kesalahan sintaks; kurung tanpa isi tidak punya bentuk yang sah:
SELECT id, nama FROM produk WHERE kategori IN ();Output yang Dihasilkan:
psql:in-kosong.sql:1: ERROR: syntax error at or near ")"
LINE 1: SELECT id, nama FROM produk WHERE kategori IN ();
^PostgreSQL menolaknya sebelum query sempat dieksekusi. Daftar yang isinya baru ditentukan saat runtime datang lewat jalur lain: IN (subquery). Ketika subquery-nya tidak mengembalikan satu baris pun, sintaksnya tetap sah — dan hasilnya mengikuti logika “tidak ada yang bisa cocok”:
SELECT id, nama FROM produk
WHERE kategori IN (SELECT kategori FROM produk WHERE kategori = 'mainan')
ORDER BY id;
SELECT id, nama FROM produk
WHERE kategori NOT IN (SELECT kategori FROM produk WHERE kategori = 'mainan')
ORDER BY id;Output yang Dihasilkan:
id | nama
----+------
(0 rows)
id | nama
----+------------------
1 | Buku Tulis 58
2 | Pulpen Hitam
3 | Kertas HVS A4
4 | Stapler Besar
5 | Amplop Coklat
6 | Penghapus
7 | Kalkulator Solar
8 | Penggaris 30cm
(8 rows)Subquery SELECT kategori FROM produk WHERE kategori = 'mainan' menghasilkan daftar kosong — tidak ada produk mainan di Toko Maju Jaya. Konsekuensinya: IN terhadap daftar kosong menjaring nol baris (tidak ada kategori yang bisa cocok dengan apa pun), sedangkan NOT IN terhadap daftar kosong meloloskan semua delapan produk — “bukan mainan” terpenuhi oleh semua kategori yang ada. Perilaku ini kelihatan aneh, tetapi konsisten dengan logika <> berantai: tanpa satu pun perbandingan yang gagal, semua baris lolos.
Jebakan NULL pada NOT IN #
Sekarang jebakan utamanya. Aturan mainnya satu kalimat: NOT IN mengembalikan nol baris bila daftarnya berisi NULL. Versi literalnya dulu, baru versi subquery-nya yang lebih sering terjadi di dunia nyata:
SELECT id, nama, kategori FROM produk
WHERE kategori NOT IN ('kertas', NULL)
ORDER BY id;
CREATE TABLE daftar_hemat (
nama text
);
INSERT INTO daftar_hemat (nama) VALUES
('Pulpen Hitam'),
('Kertas HVS A4'),
(NULL);
SELECT id, nama FROM produk
WHERE nama NOT IN (SELECT nama FROM daftar_hemat)
ORDER BY id;
SELECT id, nama FROM produk
WHERE nama NOT IN (SELECT nama FROM daftar_hemat WHERE nama IS NOT NULL)
ORDER BY id;Output yang Dihasilkan:
id | nama | kategori
----+------+----------
(0 rows)
CREATE TABLE
INSERT 0 3
id | nama
----+------
(0 rows)
id | nama
----+------------------
1 | Buku Tulis 58
4 | Stapler Besar
5 | Amplop Coklat
6 | Penghapus
7 | Kalkulator Solar
8 | Penggaris 30cm
(6 rows)Mengapa nol baris? Mekanismenya terbuka lewat definisi NOT IN sebagai rantai <> yang dihubungkan AND. Kondisi kategori NOT IN ('kertas', NULL) dibaca PostgreSQL sebagai:
kategori <> 'kertas' AND kategori <> NULLPerbandingan kedua — kategori <> NULL — selalu bernilai NULL untuk setiap baris (aturan chapter Operator Perbandingan). Seperti di tabel kebenaran AND di chapter Operator Logika, true AND NULL = NULL dan false AND NULL = false — tidak ada kombinasi yang menghasilkan true. Seluruh kondisi mati untuk semua baris, dan WHERE meloloskan nol baris tanpa protes apa pun.
Skenario daftar_hemat memperlihatkan versi dunia nyata: Budi membuat daftar produk hemat (dua nama terisi, satu baris masih NULL karena belum diputuskan), lalu mencari produk yang tidak ada di daftar itu. Hasilnya nol baris — padahal seharusnya enam produk muncul. Query perbaikannya ada di baris terakhir file: saring NULL keluar dari subquery dengan WHERE nama IS NOT NULL, dan keenam produk langsung muncul. Operator IS NULL/IS NOT NULL dibahas tuntas di chapter IS NULL; untuk kasus anti-join yang lebih besar, NOT EXISTS (Section Subquery dan CTE) adalah alternatif yang kebal NULL.
Dengan IN kita bisa menyaring terhadap daftar nilai diskrit. Bagaimana dengan rentang — “harga antara dua ribu sampai lima ribu”, “masuk antara Januari sampai Maret”? Rantai >= ... AND ... <= bisa digantikan satu operator yang lebih deskriptif: BETWEEN, materi chapter berikutnya.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
buat-tabel.sql
Unduh berkasCREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
kategori text NOT NULL,
harga numeric(10, 2) NOT NULL,
stok integer NOT NULL,
aktif boolean NOT NULL,
tanggal_masuk date,
promo text
);
INSERT INTO produk (id, nama, kategori, harga, stok, aktif, tanggal_masuk, promo) VALUES
(1, 'Buku Tulis 58', 'alat tulis', 4500, 120, true, DATE '2025-11-10', NULL),
(2, 'Pulpen Hitam', 'alat tulis', 3000, 200, true, DATE '2025-12-01', 'Diskon 10%'),
(3, 'Kertas HVS A4', 'kertas', 55000, 40, true, DATE '2026-01-05', 'Diskon 50%'),
(4, 'Stapler Besar', 'alat tulis', 24000, 15, false, DATE '2026-01-20', NULL),
(5, 'Amplop Coklat', 'kertas', 1500, 500, true, DATE '2026-02-14', NULL),
(6, 'Penghapus', 'alat tulis', 2000, 0, false, DATE '2026-02-14', NULL),
(7, 'Kalkulator Solar', 'elektronik', 185000, 8, true, DATE '2026-03-02', 'Diskon 25%'),
(8, 'Penggaris 30cm', 'alat tulis', 3500, 85, true, DATE '2026-03-15', 'Diskon 5%');
in-dasar.sql
Unduh berkasSELECT id, nama, kategori FROM produk
WHERE kategori IN ('kertas', 'elektronik')
ORDER BY id;
SELECT id, nama FROM produk
WHERE nama IN ('Penghapus', 'Pulpen Hitam', 'Kalkulator Solar')
ORDER BY id;
SELECT id, nama, kategori FROM produk
WHERE kategori NOT IN ('kertas', 'elektronik')
ORDER BY id;
in-kosong.sql
Unduh berkasSELECT id, nama FROM produk WHERE kategori IN ();
in-vs-or.sql
Unduh berkasSELECT id, nama, harga FROM produk
WHERE harga = 4500 OR harga = 55000
ORDER BY id;
SELECT id, nama, harga FROM produk
WHERE harga IN (4500, 55000)
ORDER BY id;
not-in-null.sql
Unduh berkasSELECT id, nama, kategori FROM produk
WHERE kategori NOT IN ('kertas', NULL)
ORDER BY id;
CREATE TABLE daftar_hemat (
nama text
);
INSERT INTO daftar_hemat (nama) VALUES
('Pulpen Hitam'),
('Kertas HVS A4'),
(NULL);
SELECT id, nama FROM produk
WHERE nama NOT IN (SELECT nama FROM daftar_hemat)
ORDER BY id;
SELECT id, nama FROM produk
WHERE nama NOT IN (SELECT nama FROM daftar_hemat WHERE nama IS NOT NULL)
ORDER BY id;
subquery-kosong.sql
Unduh berkasSELECT id, nama FROM produk
WHERE kategori IN (SELECT kategori FROM produk WHERE kategori = 'mainan')
ORDER BY id;
SELECT id, nama FROM produk
WHERE kategori NOT IN (SELECT kategori FROM produk WHERE kategori = 'mainan')
ORDER BY id;