Operator AND, OR, dan NOT masing-masing sudah kita pahami di chapter Operator Logika — kejutannya muncul ketika ketiganya bercampur dalam satu WHERE. SQL punya aturan precedence (urutan kekuatan operator) yang sama rohnya dengan aritmetika: AND dievaluasi lebih dulu daripada OR, seperti perkalian mendahului penjumlahan. Aturan ini membuat query tanpa tanda kurung bisa berarti sesuatu yang sama sekali berbeda dari maksud penulisnya — dan bug semacam ini tidak menghasilkan error apa pun, hanya hasil yang salah secara diam-diam. Pada chapter ini kita membedah jebakan itu dengan satu skenario nyata, lalu melihat dua pola penulisan ulang yang membuat kondisi campuran jadi sulit salah: tanda kurung eksplisit dan operator IN.

Menyiapkan Tabel Latihan #

Tabel produk Toko Maju Jaya yang sama kembali dipakai — delapan baris, tiga kategori, dua produk tidak aktif. Komposisi kategori dan aktif di data ini sengaja dibuat bervariasi (ada kertas aktif, kertas apa adanya, alat tulis aktif, dan alat tulis tidak aktif) supaya perbedaan pengelompokan kondisi terlihat tajam:

sql buat-tabel.sql
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:

text
CREATE TABLE
INSERT 0 8

Skenario: Laporan Produk yang Ditarik #

Skenarionya begini: manajer Toko Maju Jaya meminta Budi membuat laporan “produk kategori kertas atau alat tulis yang sudah tidak aktif”. Maksudnya jelas — kandidat yang boleh masuk adalah produk kertas tidak aktif dan produk alat tulis tidak aktif. Di tabel kita ada dua baris seperti itu: Stapler Besar dan Penghapus (keduanya aktif = false). Budi menulis kondisinya apa adanya, tanpa tanda kurung:

SELECT id, nama, kategori, aktif FROM produk
WHERE kategori = 'kertas' OR kategori = 'alat tulis' AND aktif = false
ORDER BY id;

SELECT id, nama, kategori, aktif FROM produk
WHERE (kategori = 'kertas' OR kategori = 'alat tulis') AND aktif = false
ORDER BY id;

SELECT id, nama, kategori, aktif FROM produk
WHERE kategori = 'kertas' OR (kategori = 'alat tulis' AND aktif = false)
ORDER BY id;

Output yang Dihasilkan:

text
 id |     nama      |  kategori  | aktif 
----+---------------+------------+-------
  3 | Kertas HVS A4 | kertas     | t
  4 | Stapler Besar | alat tulis | f
  5 | Amplop Coklat | kertas     | t
  6 | Penghapus     | alat tulis | f
(4 rows)

 id |     nama      |  kategori  | aktif 
----+---------------+------------+-------
  4 | Stapler Besar | alat tulis | f
  6 | Penghapus     | alat tulis | f
(2 rows)

 id |     nama      |  kategori  | aktif 
----+---------------+------------+-------
  3 | Kertas HVS A4 | kertas     | t
  4 | Stapler Besar | alat tulis | f
  5 | Amplop Coklat | kertas     | t
  6 | Penghapus     | alat tulis | f
(4 rows)

Tiga query, dua hasil yang berbeda:

  1. Query pertama (tanpa kurung) mengembalikan empat baris — termasuk Kertas HVS A4 dan Amplop Coklat yang aktif-nya true. Produk aktif muncul di laporan produk tidak aktif.
  2. Query kedua (kurung mengelompokkan kedua kategori) mengembalikan dua baris yang benar: Stapler Besar dan Penghapus.
  3. Query ketiga adalah bukti bagaimana PostgreSQL membaca query pertama: kategori = 'kertas' OR (kategori = 'alat tulis' AND aktif = false) — hasilnya persis sama dengan query pertama.

Akar masalahnya adalah precedence. Urutan kekuatan operator dalam kondisi SQL, dari yang terkuat:

UrutanOperatorCatatan
1tanda kurung (...)selalu menimpa aturan lain
2perbandingan (=, <>, <, <=, >, >=)menghasilkan boolean
3NOTmembalik boolean
4ANDmenggabungkan dua boolean
5ORterlemah — dievaluasi terakhir

Karena AND lebih kuat dari OR, kondisi A OR B AND C selalu dibaca A OR (B AND C). Dalam kasus Budi, produk kertas lolos lewat cabang A saja — syarat aktif = false hanya berlaku untuk alat tulis. Query tetap sah, tidak ada error, tidak ada peringatan; hasilnya saja yang salah.

Kondisi berantai pada satu kolom yang sama — kategori = 'kertas' OR kategori = 'elektronik' OR ... — cepat menjadi panjang dan makin berisiko salah letak AND di ujungnya. Operator IN meringkas rantai itu menjadi satu perbandingan terhadap daftar nilai:

SELECT id, nama, kategori FROM produk
WHERE kategori = 'kertas' OR kategori = 'elektronik'
ORDER BY id;

SELECT id, nama, kategori FROM produk
WHERE kategori IN ('kertas', 'elektronik')
ORDER BY id;

SELECT id, nama, kategori, aktif FROM produk
WHERE kategori IN ('kertas', 'alat tulis') AND aktif = false
ORDER BY id;

Output yang Dihasilkan:

text
 id |       nama       |  kategori  
----+------------------+------------
  3 | Kertas HVS A4    | kertas
  5 | Amplop Coklat    | kertas
  7 | Kalkulator Solar | elektronik
(3 rows)

 id |       nama       |  kategori  
----+------------------+------------
  3 | Kertas HVS A4    | kertas
  5 | Amplop Coklat    | kertas
  7 | Kalkulator Solar | elektronik
(3 rows)

 id |     nama      |  kategori  | aktif 
----+---------------+------------+-------
  4 | Stapler Besar | alat tulis | f
  6 | Penghapus     | alat tulis | f
(2 rows)

Dua query pertama mengembalikan hasil identik — tiga produk dari kategori kertas dan elektronik — karena IN memang bentuk ringkas dari rantai OR pada kolom yang sama. Query ketiga adalah versi final dari laporan Budi: kategori IN ('kertas', 'alat tulis') AND aktif = false. Dengan IN, tidak ada lagi dua perbandingan kategori yang harus dikurung — kelompok “kertas atau alat tulis” sudah menjadi satu kesatuan sintaksis, sehingga AND aktif = false tidak mungkin menempel pada salah satunya saja.

Dari sisi performa, kedua bentuk juga setara: planner PostgreSQL menulis ulang rantai OR kesetaraan dan IN ke bentuk internal yang sama, jadi pilihan murni urusan keterbacaan. Pembahasan IN lengkap — termasuk NOT IN beserta jebakan NULL-nya yang legendaris — ada di chapter khusus IN dan NOT IN.

NOT pada Perbandingan #

NOT membalik satu kondisi atau satu kelompok kondisi. Tiga pola berikut paling sering dipakai — membalik satu perbandingan, membalik gabungan AND, dan hukum De Morgan yang menulis ulang bentuk kedua:

SELECT id, nama, kategori FROM produk
WHERE NOT (kategori = 'alat tulis')
ORDER BY id;

SELECT id, nama, harga, aktif FROM produk
WHERE NOT (harga > 5000 AND aktif = true)
ORDER BY id;

SELECT id, nama, harga, aktif FROM produk
WHERE (NOT (harga > 5000)) OR (NOT (aktif = true))
ORDER BY id;

SELECT id, nama, promo FROM produk
WHERE NOT (promo = 'Diskon 50%')
ORDER BY id;

Output yang Dihasilkan:

text
 id |       nama       |  kategori  
----+------------------+------------
  3 | Kertas HVS A4    | kertas
  5 | Amplop Coklat    | kertas
  7 | Kalkulator Solar | elektronik
(3 rows)

 id |      nama      |  harga   | aktif 
----+----------------+----------+-------
  1 | Buku Tulis 58  |  4500.00 | t
  2 | Pulpen Hitam   |  3000.00 | t
  4 | Stapler Besar  | 24000.00 | f
  5 | Amplop Coklat  |  1500.00 | t
  6 | Penghapus      |  2000.00 | f
  8 | Penggaris 30cm |  3500.00 | t
(6 rows)

 id |      nama      |  harga   | aktif 
----+----------------+----------+-------
  1 | Buku Tulis 58  |  4500.00 | t
  2 | Pulpen Hitam   |  3000.00 | t
  4 | Stapler Besar  | 24000.00 | f
  5 | Amplop Coklat  |  1500.00 | t
  6 | Penghapus      |  2000.00 | f
  8 | Penggaris 30cm |  3500.00 | t
(6 rows)

 id |       nama       |   promo    
----+------------------+------------
  2 | Pulpen Hitam     | Diskon 10%
  7 | Kalkulator Solar | Diskon 25%
  8 | Penggaris 30cm   | Diskon 5%
(3 rows)

Mari kita bedah hasilnya:

  1. NOT (kategori = 'alat tulis') menjaring tiga produk dari kategori lain — hasil yang sama dengan kategori <> 'alat tulis'. Untuk satu perbandingan, kedua bentuk boleh dipilih sesuai selera.
  2. NOT (harga > 5000 AND aktif = true) menjaring enam produk: semua yang bukan “harga di atas 5000 sekaligus aktif”. Perhatikan Stapler Besar (harga 24000, tidak aktif) ikut lolos — salah satu syarat di dalam kurung sudah false, jadi AND-nya false, dan NOT-nya true.
  3. Query ketiga membuktikan hukum De Morgan secara hidup: NOT (a AND b) identik dengan (NOT a) OR (NOT b) — enam baris yang sama persis dengan query kedua. Hukum ini berguna saat menulis ulang kondisi negatif yang panjang menjadi rantai positif yang lebih mudah dibaca.
  4. NOT (promo = 'Diskon 50%') kembali menabrak logika tiga nilai: hanya tiga baris muncul, karena NOT NULL tetap NULL dan empat produk ber-promo NULL kandas diam-diam — persis jebakan promo != 'Diskon 50%' di chapter WHERE.

Kurung eksplisit dan IN menutup celah bug precedence untuk sebagian besar kasus. Bab berikutnya masuk lebih dalam ke IN itu sendiri: daftar nilai literal, subquery, dan NOT IN — operator dengan satu jebakan NULL paling terkenal di dunia SQL yang membuat query mengembalikan nol baris tanpa error.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

buat-tabel.sql

Unduh berkas
sql
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%');

jebakan-precedence.sql

Unduh berkas
sql
SELECT id, nama, kategori, aktif FROM produk
WHERE kategori = 'kertas' OR kategori = 'alat tulis' AND aktif = false
ORDER BY id;

SELECT id, nama, kategori, aktif FROM produk
WHERE (kategori = 'kertas' OR kategori = 'alat tulis') AND aktif = false
ORDER BY id;

SELECT id, nama, kategori, aktif FROM produk
WHERE kategori = 'kertas' OR (kategori = 'alat tulis' AND aktif = false)
ORDER BY id;

not-perbandingan.sql

Unduh berkas
sql
SELECT id, nama, kategori FROM produk
WHERE NOT (kategori = 'alat tulis')
ORDER BY id;

SELECT id, nama, harga, aktif FROM produk
WHERE NOT (harga > 5000 AND aktif = true)
ORDER BY id;

SELECT id, nama, harga, aktif FROM produk
WHERE (NOT (harga > 5000)) OR (NOT (aktif = true))
ORDER BY id;

SELECT id, nama, promo FROM produk
WHERE NOT (promo = 'Diskon 50%')
ORDER BY id;

tulis-ulang-in.sql

Unduh berkas
sql
SELECT id, nama, kategori FROM produk
WHERE kategori = 'kertas' OR kategori = 'elektronik'
ORDER BY id;

SELECT id, nama, kategori FROM produk
WHERE kategori IN ('kertas', 'elektronik')
ORDER BY id;

SELECT id, nama, kategori, aktif FROM produk
WHERE kategori IN ('kertas', 'alat tulis') AND aktif = false
ORDER BY id;