Semua operator filter yang kita pakai sejauh ini — perbandingan, IN, BETWEEN, sampai regex — berbagi satu kelemahan: begitu bertemu nilai NULL, hasilnya bukan true atau false, melainkan NULL itu sendiri, dan klausa WHERE hanya meloloskan baris yang kondisinya true. Akibatnya, baris dengan data yang belum terisi bisa hilang dari hasil tanpa error dan tanpa peringatan. Di dunia nyata, data yang kosong justru sering menjadi yang paling perlu dicari: pelanggan yang belum dicatat nomor teleponnya, pesanan yang belum punya tanggal kirim. Chapter ini membahas operator khusus untuk memburu nilai yang hilang — IS NULL dan IS NOT NULL — beserta IS DISTINCT FROM, perbandingan yang memperlakukan NULL sebagai nilai yang bisa diadu secara adil.

Menyiapkan Tabel Latihan #

Tabel pelanggan berikut sengaja memuat banyak lubang: kolom telepon, diskon_persen, dan catatan semuanya boleh NULL. Perhatikan juga dua baris (Andi Wijaya dan Rudi Hartono) yang catatan-nya berisi string kosong '' — bukan NULL — karena perbedaan keduanya akan kita bedah khusus:

sql buat-tabel.sql
CREATE TABLE pelanggan (
    id            integer PRIMARY KEY,
    nama          text NOT NULL,
    email         text NOT NULL,
    telepon       text,
    diskon_persen integer,
    catatan       text
);

INSERT INTO pelanggan (id, nama, email, telepon, diskon_persen, catatan) VALUES
    (1, 'Budi Santoso', '[email protected]', '0812-3456-7890', 5,    NULL),
    (2, 'Siti Rahayu',  '[email protected]', NULL,              NULL, 'pelanggan sejak 2024'),
    (3, 'Andi Wijaya',  '[email protected]', '082156789012',    10,   ''),
    (4, 'Dewi Lestari', '[email protected]', NULL,              5,    NULL),
    (5, 'Rudi Hartono', '[email protected]', '0877123456',      NULL, ''),
    (6, 'Maya Putri',   '[email protected]', NULL,              NULL, 'sering menanyakan stok kertas'),
    (7, 'Agus Salim',   '[email protected]', '+62 812-9999-8888', 5,  NULL),
    (8, 'Rina Melati',  '[email protected]', '0215551234',      NULL, NULL);

Output yang Dihasilkan:

text
CREATE TABLE
INSERT 0 8

NULL Berarti “Tidak Ada Nilai”, Bukan Nol dan Bukan Kosong #

NULL bukan angka 0, bukan string '', dan bukan tanggal tertentu. NULL adalah penanda bahwa sebuah nilai tidak ada atau tidak diketahui — nomor telepon belum pernah ditanyakan, diskon belum ditetapkan. Karena statusnya “tidak diketahui”, SQL memperlakukan semua operasi yang menyentuh NULL dengan logika tiga nilai: hasilnya bisa true, false, atau NULL (“tidak bisa ditentukan”). Logika tiga nilai ini pernah kita buktikan di chapter Boolean pada Section Tipe Data; di sini fokusnya adalah dampaknya terhadap penyaringan baris.

Operator IS NULL dan IS NOT NULL #

Karena NULL tidak bisa dibandingkan dengan =, SQL menyediakan operator khusus yang memeriksa keberadaan nilai itu sendiri. IS NULL menjawab “apakah kolom ini tidak memiliki nilai?”, sedangkan IS NOT NULL menjawab kebalikannya:

-- Pelanggan yang belum memiliki nomor telepon tersimpan.
SELECT nama, telepon
FROM pelanggan
WHERE telepon IS NULL
ORDER BY id;

-- Pelanggan yang sudah punya program diskon.
SELECT nama, diskon_persen
FROM pelanggan
WHERE diskon_persen IS NOT NULL
ORDER BY id;

Output yang Dihasilkan:

text
     nama     | telepon 
--------------+---------
 Siti Rahayu  | 
 Dewi Lestari | 
 Maya Putri   | 
(3 rows)

     nama     | diskon_persen 
--------------+---------------
 Budi Santoso |             5
 Andi Wijaya  |            10
 Dewi Lestari |             5
 Agus Salim   |             5
(4 rows)

Query pertama menemukan tiga pelanggan yang nomornya belum tercatat; query kedua menemukan empat pelanggan yang sudah punya diskon. Perhatikan output kolom telepon: baris NULL ditampilkan psql sebagai sel kosong — bentuknya persis sama dengan string kosong di layar, meski nilainya berbeda total. Inilah alasan Anda tidak boleh menebak NULL dari tampilan, selalu periksa dengan IS NULL.

Jebakan NULL = NULL #

Intuisi mengatakan “NULL sama dengan NULL”. SQL berkata lain: dua nilai yang tidak diketahui tidak bisa dinyatakan sama, sehingga NULL = NULL menghasilkan NULL — bukan true. Konsekuensinya di klausa WHERE sangat senyap:

-- Perbandingan NULL = NULL tidak menghasilkan true, melainkan NULL.
SELECT NULL = NULL AS hasil;

-- Bukti bahwa hasilnya benar-benar NULL (bukan false):
SELECT (NULL = NULL) IS NULL AS ternyata_null,
       (NULL = NULL) IS TRUE  AS ternyata_true;

-- Konsekuensinya di WHERE: kondisi yang tidak TRUE akan meng-gugurkan baris.
-- Query berikut mengembalikan nol baris — tanpa error, tanpa peringatan.
SELECT nama
FROM pelanggan
WHERE telepon = NULL
ORDER BY id;

-- Bentuk yang benar untuk membandingkan dengan "tidak ada nilai":
SELECT nama
FROM pelanggan
WHERE telepon IS NULL
ORDER BY id;

Output yang Dihasilkan:

text
 hasil 
-------
 
(1 row)

 ternyata_null | ternyata_true 
---------------+---------------
 t             | f
(1 row)

 nama 
------
(0 rows)

     nama     
--------------
 Siti Rahayu
 Dewi Lestari
 Maya Putri
(3 rows)

Empat fakta dari hasil di atas:

  1. SELECT NULL = NULL menampilkan kolom hasil yang kosong — itulah tampilan NULL di psql.
  2. (NULL = NULL) IS NULL menghasilkan t — bukti resmi bahwa hasil persamaannya adalah NULL, bukan true atau false.
  3. WHERE telepon = NULL mengembalikan 0 baris tanpa pesan error apa pun. Kondisi NULL tidak pernah menjadi true, jadi semua baris gugur — bug klasik yang membuat laporan “kehilangan” data secara diam-diam.
  4. WHERE telepon IS NULL mengembalikan tiga baris yang benar.

String Kosong ’’ Bukan NULL #

Pemula sering menganggap string kosong '' dan NULL sama saja. Untuk PostgreSQL keduanya berbeda: '' adalah nilai teks nyata sepanjang nol karakter, sedangkan NULL adalah ketiadaan nilai. Filter yang salah menyasar salah satu jenisnya akan melewatkan baris yang dicari:

-- IS NULL hanya menangkap nilai NULL...
SELECT nama, catatan
FROM pelanggan
WHERE catatan IS NULL
ORDER BY id;

-- ...sedangkan string kosong '' adalah nilai biasa, bukan NULL.
-- Perbandingan = '' hanya menangkap baris dengan string kosong.
SELECT nama, catatan
FROM pelanggan
WHERE catatan = ''
ORDER BY id;

-- length() memperjelas bedanya: NULL punya panjang NULL, '' punya panjang 0.
SELECT nama, catatan, length(catatan) AS panjang
FROM pelanggan
ORDER BY id;

Output yang Dihasilkan:

text
     nama     | catatan 
--------------+---------
 Budi Santoso | 
 Dewi Lestari | 
 Agus Salim   | 
 Rina Melati  | 
(4 rows)

     nama     | catatan 
--------------+---------
 Andi Wijaya  | 
 Rudi Hartono | 
(2 rows)

     nama     |            catatan            | panjang 
--------------+-------------------------------+---------
 Budi Santoso |                               |        
 Siti Rahayu  | pelanggan sejak 2024          |      20
 Andi Wijaya  |                               |       0
 Dewi Lestari |                               |        
 Rudi Hartono |                               |       0
 Maya Putri   | sering menanyakan stok kertas |      29
 Agus Salim   |                               |        
 Rina Melati  |                               |        
(8 rows)

Query pertama menemukan empat pelanggan dengan catatan NULL; query kedua menemukan dua pelanggan dengan catatan ''. Query ketiga memperlihatkan pembeda visualnya: kolom panjang bernilai NULL (tampil kosong) untuk catatan NULL, tetapi 0 untuk string kosong. Tim Anda perlu satu kesepakatan desain — misalnya “catatan yang belum diisi selalu NULL, form yang dikirim kosong dikonversi ke NULL” — lalu konsisten memakainya di seluruh aplikasi, karena mencampur keduanya membuat data mustahil disaring bersih.

IS DISTINCT FROM: Perbandingan yang Aman terhadap NULL #

Operator perbandingan =, <>, dan != semuanya menelan baris NULL. Untuk filter yang justru perlu memperlakukan NULL sebagai nilai yang bisa diadu, SQL standar menyediakan IS DISTINCT FROM dan kebalikannya IS NOT DISTINCT FROM:

EkspresiHasilPenjelasan
NULL = NULLNULLdua hal tak diketahui tak bisa disamakan
NULL <> 5NULLperbandingan biasa menelan NULL
NULL IS DISTINCT FROM 5trueNULL dianggap berbeda dari nilai apa pun
NULL IS DISTINCT FROM NULLfalsedua NULL dianggap tidak berbeda
NULL IS NOT DISTINCT FROM NULLtruekebalikan baris di atasnya
-- Operator biasa <> menjatuhkan baris yang diskonnya NULL (hasil NULL ≠ TRUE).
SELECT nama, diskon_persen
FROM pelanggan
WHERE diskon_persen <> 5
ORDER BY id;

-- IS DISTINCT FROM memperlakukan NULL sebagai nilai yang bisa dibandingkan:
-- NULL dianggap "berbeda dari 5", sehingga baris tanpa diskon ikut diloloskan.
SELECT nama, diskon_persen
FROM pelanggan
WHERE diskon_persen IS DISTINCT FROM 5
ORDER BY id;

-- IS NOT DISTINCT FROM adalah kebalikannya: mencocokkan nilai yang sama,
-- dan dua NULL dianggap "sama" (tidak berbeda).
SELECT NULL IS DISTINCT FROM NULL AS null_vs_null,
       NULL IS NOT DISTINCT FROM NULL AS null_sama_null,
       5 IS DISTINCT FROM 5 AS lima_vs_lima;

-- Pencarian "persis tanpa diskon" (diskonnya NULL) juga bisa memakai
-- IS NOT DISTINCT FROM NULL:
SELECT nama, diskon_persen
FROM pelanggan
WHERE diskon_persen IS NOT DISTINCT FROM NULL
ORDER BY id;

Output yang Dihasilkan:

text
    nama     | diskon_persen 
-------------+---------------
 Andi Wijaya |            10
(1 row)

     nama     | diskon_persen 
--------------+---------------
 Siti Rahayu  |              
 Andi Wijaya  |            10
 Rudi Hartono |              
 Maya Putri   |              
 Rina Melati  |              
(5 rows)

 null_vs_null | null_sama_null | lima_vs_lima 
--------------+----------------+--------------
 f            | t              | f
(1 row)

     nama     | diskon_persen 
--------------+---------------
 Siti Rahayu  |              
 Rudi Hartono |              
 Maya Putri   |              
 Rina Melati  |              
(4 rows)

Kontras dua query pertama adalah inti section ini: diskon_persen <> 5 hanya mengembalikan satu baris (Andi Wijaya, diskon 10) karena empat pelanggan berdiskon NULL gugur diam-diam, sedangkan diskon_persen IS DISTINCT FROM 5 mengembalikan lima baris — pelanggan berdiskon 10 sekaligus seluruh pelanggan yang belum punya diskon. Query terakhir memperlihatkan idiom IS NOT DISTINCT FROM NULL yang setara dengan IS NULL, berguna saat pola filter dibangun secara dinamis oleh aplikasi.

Menghitung Baris yang NULL #

Untuk menghitung lubang data, Anda perlu tahu satu perilaku penting function agregat: count(*) menghitung semua baris, tetapi count(kolom) hanya menghitung baris yang kolomnya bukan NULL:

sql hitung-null.sql
-- count(kolom) hanya menghitung baris yang kolomnya bukan NULL.
SELECT count(*)          AS total_baris,
       count(telepon)    AS dengan_telepon,
       count(diskon_persen) AS dengan_diskon
FROM pelanggan;

-- Selisih keduanya = jumlah baris yang nilainya NULL.
SELECT count(*) - count(telepon) AS tanpa_telepon,
       count(*) - count(diskon_persen) AS tanpa_diskon
FROM pelanggan;

-- catatan '' ikut dihitung oleh count(catatan) karena string kosong tetap nilai.
SELECT count(catatan) AS catatan_terisi
FROM pelanggan;

Output yang Dihasilkan:

text
 total_baris | dengan_telepon | dengan_diskon 
-------------+----------------+---------------
           8 |              5 |             4
(1 row)

 tanpa_telepon | tanpa_diskon 
---------------+--------------
             3 |            4
(1 row)

 catatan_terisi 
----------------
              4
(1 row)

Dari delapan pelanggan, hanya lima yang punya telepon dan empat yang punya diskon — selisihnya dengan count(*) langsung menjadi jumlah baris NULL. Query ketiga menghitung count(catatan) sebesar 4: dua catatan berisi teks dan dua string kosong '' ikut terhitung, karena bagi count() string kosong tetaplah nilai. Pembahasan lengkap function agregat (count, sum, avg, dan kawan-kawannya) menanti di Section Agregasi Data — di sini kita cukup memakai count sebagai alat hitung baris NULL.

Mampu memeriksa keberadaan nilai melengkapi kemampuan Anda menyaring isi nilai. Pertanyaan berikutnya berada di arah sebaliknya: bagaimana bila hasil query penuh dengan baris kembar yang membuat laporan berulang? Chapter DISTINCT berikutnya membersihkan duplikasi itu.

Referensi #

Berkas Sumber

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

buat-tabel.sql

Unduh berkas
sql
CREATE TABLE pelanggan (
    id            integer PRIMARY KEY,
    nama          text NOT NULL,
    email         text NOT NULL,
    telepon       text,
    diskon_persen integer,
    catatan       text
);

INSERT INTO pelanggan (id, nama, email, telepon, diskon_persen, catatan) VALUES
    (1, 'Budi Santoso', '[email protected]', '0812-3456-7890', 5,    NULL),
    (2, 'Siti Rahayu',  '[email protected]', NULL,              NULL, 'pelanggan sejak 2024'),
    (3, 'Andi Wijaya',  '[email protected]', '082156789012',    10,   ''),
    (4, 'Dewi Lestari', '[email protected]', NULL,              5,    NULL),
    (5, 'Rudi Hartono', '[email protected]', '0877123456',      NULL, ''),
    (6, 'Maya Putri',   '[email protected]', NULL,              NULL, 'sering menanyakan stok kertas'),
    (7, 'Agus Salim',   '[email protected]', '+62 812-9999-8888', 5,  NULL),
    (8, 'Rina Melati',  '[email protected]', '0215551234',      NULL, NULL);

hitung-null.sql

Unduh berkas
sql
-- count(kolom) hanya menghitung baris yang kolomnya bukan NULL.
SELECT count(*)          AS total_baris,
       count(telepon)    AS dengan_telepon,
       count(diskon_persen) AS dengan_diskon
FROM pelanggan;

-- Selisih keduanya = jumlah baris yang nilainya NULL.
SELECT count(*) - count(telepon) AS tanpa_telepon,
       count(*) - count(diskon_persen) AS tanpa_diskon
FROM pelanggan;

-- catatan '' ikut dihitung oleh count(catatan) karena string kosong tetap nilai.
SELECT count(catatan) AS catatan_terisi
FROM pelanggan;

is-distinct-from.sql

Unduh berkas
sql
-- Operator biasa <> menjatuhkan baris yang diskonnya NULL (hasil NULL ≠ TRUE).
SELECT nama, diskon_persen
FROM pelanggan
WHERE diskon_persen <> 5
ORDER BY id;

-- IS DISTINCT FROM memperlakukan NULL sebagai nilai yang bisa dibandingkan:
-- NULL dianggap "berbeda dari 5", sehingga baris tanpa diskon ikut diloloskan.
SELECT nama, diskon_persen
FROM pelanggan
WHERE diskon_persen IS DISTINCT FROM 5
ORDER BY id;

-- IS NOT DISTINCT FROM adalah kebalikannya: mencocokkan nilai yang sama,
-- dan dua NULL dianggap "sama" (tidak berbeda).
SELECT NULL IS DISTINCT FROM NULL AS null_vs_null,
       NULL IS NOT DISTINCT FROM NULL AS null_sama_null,
       5 IS DISTINCT FROM 5 AS lima_vs_lima;

-- Pencarian "persis tanpa diskon" (diskonnya NULL) juga bisa memakai
-- IS NOT DISTINCT FROM NULL:
SELECT nama, diskon_persen
FROM pelanggan
WHERE diskon_persen IS NOT DISTINCT FROM NULL
ORDER BY id;

null-perbandingan.sql

Unduh berkas
sql
-- Perbandingan NULL = NULL tidak menghasilkan true, melainkan NULL.
SELECT NULL = NULL AS hasil;

-- Bukti bahwa hasilnya benar-benar NULL (bukan false):
SELECT (NULL = NULL) IS NULL AS ternyata_null,
       (NULL = NULL) IS TRUE  AS ternyata_true;

-- Konsekuensinya di WHERE: kondisi yang tidak TRUE akan meng-gugurkan baris.
-- Query berikut mengembalikan nol baris — tanpa error, tanpa peringatan.
SELECT nama
FROM pelanggan
WHERE telepon = NULL
ORDER BY id;

-- Bentuk yang benar untuk membandingkan dengan "tidak ada nilai":
SELECT nama
FROM pelanggan
WHERE telepon IS NULL
ORDER BY id;

periksa-null.sql

Unduh berkas
sql
-- Pelanggan yang belum memiliki nomor telepon tersimpan.
SELECT nama, telepon
FROM pelanggan
WHERE telepon IS NULL
ORDER BY id;

-- Pelanggan yang sudah punya program diskon.
SELECT nama, diskon_persen
FROM pelanggan
WHERE diskon_persen IS NOT NULL
ORDER BY id;

string-kosong-vs-null.sql

Unduh berkas
sql
-- IS NULL hanya menangkap nilai NULL...
SELECT nama, catatan
FROM pelanggan
WHERE catatan IS NULL
ORDER BY id;

-- ...sedangkan string kosong '' adalah nilai biasa, bukan NULL.
-- Perbandingan = '' hanya menangkap baris dengan string kosong.
SELECT nama, catatan
FROM pelanggan
WHERE catatan = ''
ORDER BY id;

-- length() memperjelas bedanya: NULL punya panjang NULL, '' punya panjang 0.
SELECT nama, catatan, length(catatan) AS panjang
FROM pelanggan
ORDER BY id;