NULL adalah cara database mengatakan “nilai ini tidak diketahui” — bukan nol, bukan string kosong, bukan apa pun. Ketika NULL bersarang di kolom email atau poin, query mulai berperilaku aneh: tampilan menjadi kosong di tempat yang tidak jelas alasannya, perhitungan ikut menjadi NULL, dan ORDER BY menaruh baris tersebut di posisi yang tidak terduga. PostgreSQL menyediakan keluarga kecil fungsi yang justru memanfaatkan NULL sebagai alat kendali: COALESCE menyediakan nilai pengganti, NULLIF sengaja menghasilkan NULL untuk mematikan perhitungan yang berbahaya, dan GREATEST/LEAST memilih nilai ekstrem dengan aturan khusus terhadap NULL. Chapter ini menguji semuanya pada tabel pelanggan yang penuh lubang NULL.

Tabel Latihan dengan NULL #

Empat pelanggan berikut sengaja tidak lengkap: Siti belum memberi email dan poinnya belum dihitung, Toko Maju Jaya mengisi telepon dengan string kosong, dan Ahmad hanya menyisakan poinnya:

sql buat-tabel.sql
-- tabel pelanggan dengan beberapa kolom yang boleh NULL
CREATE TABLE pelanggan (
    id       integer PRIMARY KEY,
    nama     text NOT NULL,
    email    text,
    telepon  text,
    poin     integer
);

INSERT INTO pelanggan (id, nama, email, telepon, poin) VALUES
    (1, 'Budi', '[email protected]', '0812-1111-2222', 120),
    (2, 'Siti', NULL, '0813-3333-4444', NULL),
    (3, 'Toko Maju Jaya', '[email protected]', '', NULL),
    (4, 'Ahmad', NULL, NULL, 45);

SELECT id, nama, email, telepon, poin
FROM pelanggan
ORDER BY id;

Output yang Dihasilkan:

text
CREATE TABLE
INSERT 0 4
 id |      nama      |       email       |    telepon     | poin 
----+----------------+-------------------+----------------+------
  1 | Budi           | [email protected]  | 0812-1111-2222 |  120
  2 | Siti           |                   | 0813-3333-4444 |     
  3 | Toko Maju Jaya | [email protected] |                |     
  4 | Ahmad          |                   |                |   45
(4 rows)

Dalam tampilan psql, NULL muncul sebagai ruang kosong — kolom email milik Siti dan Ahmad tampak seperti tidak diisi, dan itulah memang kebenarannya. Perhatikan juga baris ketiga: '[email protected]' mengisi email, tetapi kolom telepon berisi string kosong '', bukan NULL. Keduanya berbeda — string kosong adalah nilai yang diketahui isinya (yaitu tidak ada karakter), sedangkan NULL adalah nilai yang tidak diketahui sama sekali. Perbedaan ini akan dimanfaatkan oleh NULLIF di bagian berikutnya.

COALESCE: Nilai Pengganti Berantai #

COALESCE(a, b, c, ...) mengevaluasi argumennya satu per satu dari kiri dan mengembalikan argumen pertama yang tidak NULL; jika semuanya NULL, hasilnya NULL. Fungsi ini adalah cara paling ringkas untuk menyediakan nilai cadangan:

EkspresiHasilAlasan
COALESCE(NULL, NULL, 'ketiga')'ketiga'Dua argumen pertama NULL
COALESCE('a', 'b')'a'Argumen pertama sudah tidak NULL
COALESCE(NULL, 0)0Pengganti untuk perhitungan
COALESCE(NULL, NULL)NULLTidak ada yang selamat
sql coalesce.sql
-- COALESCE mengembalikan argumen pertama yang tidak NULL
SELECT COALESCE(NULL, NULL, 'nilai ketiga') AS dari_literal;

-- nilai tampilan pengganti untuk kolom yang boleh NULL
SELECT nama,
       COALESCE(email, '(email belum diisi)') AS email_tampil
FROM pelanggan
ORDER BY nama;

-- rantai pengganti berurutan: coba email dulu, lalu telepon
SELECT nama,
       COALESCE(email, telepon, 'belum ada kontak') AS kontak
FROM pelanggan
ORDER BY nama;

-- pengganti angka: NULL poin dibaca sebagai nol
SELECT nama,
       COALESCE(poin, 0) AS poin_aman,
       COALESCE(poin, 0) * 2 AS poin_ganda
FROM pelanggan
ORDER BY nama;

Output yang Dihasilkan:

text
 dari_literal 
--------------
 nilai ketiga
(1 row)

      nama      |    email_tampil     
----------------+---------------------
 Ahmad          | (email belum diisi)
 Budi           | [email protected]
 Siti           | (email belum diisi)
 Toko Maju Jaya | [email protected]
(4 rows)

      nama      |      kontak       
----------------+-------------------
 Ahmad          | belum ada kontak
 Budi           | [email protected]
 Siti           | 0813-3333-4444
 Toko Maju Jaya | [email protected]
(4 rows)

      nama      | poin_aman | poin_ganda 
----------------+-----------+------------
 Ahmad          |        45 |         90
 Budi           |       120 |        240
 Siti           |         0 |          0
 Toko Maju Jaya |         0 |          0
(4 rows)

Tiga pola pemakaian terlihat berurutan di atas. Pertama, pengganti tampilan: kolom email_tampil tidak lagi menampilkan kekosongan, melainkan teks (email belum diisi). Kedua, rantai prioritas: kolom kontak memilih email bila ada, jatuh ke telepon bila email NULL, dan berakhir di teks cadangan bila keduanya kosong — Siti yang punya telepon mendapat '0813-3333-4444', sementara Ahmad yang tidak punya keduanya mendapat 'belum ada kontak'. Ketiga, pengganti angka: COALESCE(poin, 0) * 2 menghasilkan angka untuk semua baris, termasuk 0 untuk Siti dan Toko Maju Jaya — tanpa netralisasi ini, hasil perkaliannya akan NULL karena NULL menular ke seluruh ekspresi aritmetika.

NULLIF: Menghasilkan NULL Secara Sengaja #

NULLIF(a, b) bekerja ke arah sebaliknya dari COALESCE: kedua argumen dibandingkan, dan jika sama, hasilnya NULL; jika berbeda, hasilnya argumen pertama. Kedengarannya aneh, tetapi dua pola berikut menjelaskan mengapa fungsi kecil ini sangat penting:

sql nullif.sql
-- NULLIF(a, b): menghasilkan NULL ketika a sama dengan b
SELECT NULLIF('sama', 'sama') AS hasil_null,
       NULLIF('beda', 'sama') AS hasil_tetap;

-- pola klasik: melindungi pembagian dari pembagi nol
SELECT 100 / NULLIF(0, 0) AS dibagi_nullif;

-- normalisasi string kosong menjadi NULL
SELECT NULLIF('', '') AS kosong_jadi_null,
       NULLIF('Budi', '') AS teks_normal;

-- di konteks tabel: string kosong telepon dinormalisasi menjadi NULL
SELECT nama,
       telepon,
       NULLIF(telepon, '') AS telepon_bersih
FROM pelanggan
ORDER BY nama;

Output yang Dihasilkan:

text
 hasil_null | hasil_tetap 
------------+-------------
            | beda
(1 row)

 dibagi_nullif 
---------------
              
(1 row)

 kosong_jadi_null | teks_normal 
------------------+-------------
                  | Budi
(1 row)

      nama      |    telepon     | telepon_bersih 
----------------+----------------+----------------
 Ahmad          |                | 
 Budi           | 0812-1111-2222 | 0812-1111-2222
 Siti           | 0813-3333-4444 | 0813-3333-4444
 Toko Maju Jaya |                | 
(4 rows)

Pola pertama dan paling terkenal adalah pelindung pembagian. Tanpa pelindung, pembagian oleh nol adalah error fatal yang menghentikan seluruh query:

sql bagi-nol.sql
-- tanpa pelindung: pembagian oleh nol menghentikan seluruh query
SELECT 100 / 0;

Output yang Dihasilkan:

text
psql:bagi-nol.sql:2: ERROR:  division by zero

Error division by zero membuktikan masalahnya. Dengan NULLIF(0, 0) di posisi pembagi, nol berubah menjadi NULL sebelum pembagian berjalan — dan di PostgreSQL, 100 / NULL bukan error melainkan hasil NULL. Query tetap hidup, baris dengan pembagi nol hanya menampilkan hasil kosong. Pola lengkapnya pada data nyata: SELECT total / NULLIF(jumlah, 0) AS rata_per_item FROM ... — baris dengan jumlah = 0 menghasilkan NULL, sisanya dihitung normal.

Pola kedua adalah normalisasi: NULLIF(telepon, '') mengubah string kosong '' milik Toko Maju Jaya menjadi NULL di kolom telepon_bersih, karena keduanya memang maknanya sama (“tidak ada nomor”). Menggabungkan arah biasa: COALESCE(NULLIF(nilai, ''), nilai_pengganti) menormalkan string kosong lalu memberi pengganti — satu baris yang menangani dua jenis “kosong” sekaligus.

GREATEST dan LEAST di Sekitar NULL #

Fungsi GREATEST(...) dan LEAST(...) memilih nilai terbesar dan terkecil dari daftar argumen. Perilakunya terhadap NULL berbeda dari operator pembanding — dan lebih ramah:

EkspresiHasilAturan
GREATEST(3, NULL, 7)7NULL di dalam daftar diabaikan
LEAST('b', NULL, 'a')'a'NULL di dalam daftar diabaikan
GREATEST(NULL, NULL)NULLSemua NULL barulah hasil NULL
-- GREATEST dan LEAST mengabaikan NULL di dalam daftar
SELECT GREATEST(3, NULL, 7) AS terbesar,
       LEAST('b', NULL, 'a') AS terkecil;

-- jika seluruh argumen NULL, hasilnya NULL
SELECT GREATEST(NULL, NULL) IS NULL AS semua_null;

-- di konteks tabel: NULL pada poin tidak menular ke hasil
SELECT nama,
       poin,
       GREATEST(poin, 50) AS poin_minimal_50,
       LEAST(poin, 100) AS poin_maksimal_100
FROM pelanggan
ORDER BY nama;

Output yang Dihasilkan:

text
 terbesar | terkecil 
----------+----------
        7 | a
(1 row)

 semua_null 
------------
 t
(1 row)

      nama      | poin | poin_minimal_50 | poin_maksimal_100 
----------------+------+-----------------+-------------------
 Ahmad          |   45 |              50 |                45
 Budi           |  120 |             120 |               100
 Siti           |      |              50 |               100
 Toko Maju Jaya |      |              50 |               100
(4 rows)

Bandingkan dua kolom hasil pada tabel: GREATEST(poin, 50) untuk Budi menghasilkan 120 (poinnya menang), sedangkan untuk Siti yang poinnya NULL hasilnya 50 — NULL diabaikan, angka 50 dianggap pemenangnya. Perilaku “abaikan NULL” ini kebalikan dari operator > dan < yang menghasilkan NULL ketika salah satu operannya NULL. Konsekuensinya penting untuk dipahami: fungsi ini cocok untuk memasang batas nilai (GREATEST(harga_asli, harga_minimum)), tetapi tidak bisa dipakai untuk mendeteksi NULL — gunakan IS NULL atau COALESCE untuk itu.

COALESCE di ORDER BY dan Kolom Hitungan #

Kedua fungsi ini bukan warga tetap daftar SELECT saja — mereka bisa muncul di ORDER BY untuk mengatur nasib baris NULL, dan di kolom hitungan untuk menetralkan NULL sebelum berhitung:

-- tanpa COALESCE: baris dengan email NULL tampil paling awal (ASC = NULLS FIRST)
SELECT nama, email
FROM pelanggan
ORDER BY email, nama;

-- dengan COALESCE: baris tanpa email didorong ke akhir daftar
SELECT nama, email
FROM pelanggan
ORDER BY COALESCE(email, 'zzz-tanpa-email'), nama;

-- kolom hitungan dengan NULL yang sudah dinetralkan
SELECT nama,
       COALESCE(poin, 0) AS poin_netral,
       COALESCE(poin, 0) * 500 AS nilai_poin
FROM pelanggan
ORDER BY COALESCE(poin, 0) DESC, nama;

Output yang Dihasilkan:

text
      nama      |       email       
----------------+-------------------
 Budi           | [email protected]
 Toko Maju Jaya | [email protected]
 Ahmad          | 
 Siti           | 
(4 rows)

      nama      |       email       
----------------+-------------------
 Budi           | [email protected]
 Toko Maju Jaya | [email protected]
 Ahmad          | 
 Siti           | 
(4 rows)

      nama      | poin_netral | nilai_poin 
----------------+-------------+------------
 Budi           |         120 |      60000
 Ahmad          |          45 |      22500
 Siti           |           0 |          0
 Toko Maju Jaya |           0 |          0
(4 rows)

Query pertama memperlihatkan perilaku bawaan yang kerap mengejutkan: pada urutan naik, PostgreSQL menempatkan NULL sebelum semua nilai — Siti dan Ahmad melompat ke baris paling atas. Query kedua membaliknya tanpa menyentuh data: COALESCE(email, 'zzz-tanpa-email') memberi baris tanpa email kunci urutan yang pasti berada di belakang semua alamat sungguhan, sehingga baris NULL jatuh ke akhir daftar (alternatif resminya adalah sufiks NULLS LAST, dibahas di section Filtering dan Sorting). Query ketiga adalah pola laporan yang paling sering dipakai: COALESCE(poin, 0) * 500 menghitung nilai poin untuk semua pelanggan tanpa membiarkan NULL merusak perkalian, lalu ORDER BY COALESCE(poin, 0) DESC memakai netralisasi yang sama supaya urutan hasil konsisten.

Dengan COALESCE, NULLIF, dan GREATEST/LEAST, NULL sudah bisa dikendalikan di hampir semua situasi. Masih ada satu pola kendali nilai yang belum tersentuh: memilih hasil berdasarkan kondisi — “jika stok habis tampilkan label merah, jika menipis tampilkan kuning” — dan itu pekerjaan ekspresi CASE WHEN di chapter berikutnya, Fungsi Kondisional .

Referensi #

Berkas Sumber

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

bagi-nol.sql

Unduh berkas
sql
-- tanpa pelindung: pembagian oleh nol menghentikan seluruh query
SELECT 100 / 0;

buat-tabel.sql

Unduh berkas
sql
-- tabel pelanggan dengan beberapa kolom yang boleh NULL
CREATE TABLE pelanggan (
    id       integer PRIMARY KEY,
    nama     text NOT NULL,
    email    text,
    telepon  text,
    poin     integer
);

INSERT INTO pelanggan (id, nama, email, telepon, poin) VALUES
    (1, 'Budi', '[email protected]', '0812-1111-2222', 120),
    (2, 'Siti', NULL, '0813-3333-4444', NULL),
    (3, 'Toko Maju Jaya', '[email protected]', '', NULL),
    (4, 'Ahmad', NULL, NULL, 45);

SELECT id, nama, email, telepon, poin
FROM pelanggan
ORDER BY id;

coalesce-orderby.sql

Unduh berkas
sql
-- tanpa COALESCE: baris dengan email NULL tampil paling awal (ASC = NULLS FIRST)
SELECT nama, email
FROM pelanggan
ORDER BY email, nama;

-- dengan COALESCE: baris tanpa email didorong ke akhir daftar
SELECT nama, email
FROM pelanggan
ORDER BY COALESCE(email, 'zzz-tanpa-email'), nama;

-- kolom hitungan dengan NULL yang sudah dinetralkan
SELECT nama,
       COALESCE(poin, 0) AS poin_netral,
       COALESCE(poin, 0) * 500 AS nilai_poin
FROM pelanggan
ORDER BY COALESCE(poin, 0) DESC, nama;

coalesce.sql

Unduh berkas
sql
-- COALESCE mengembalikan argumen pertama yang tidak NULL
SELECT COALESCE(NULL, NULL, 'nilai ketiga') AS dari_literal;

-- nilai tampilan pengganti untuk kolom yang boleh NULL
SELECT nama,
       COALESCE(email, '(email belum diisi)') AS email_tampil
FROM pelanggan
ORDER BY nama;

-- rantai pengganti berurutan: coba email dulu, lalu telepon
SELECT nama,
       COALESCE(email, telepon, 'belum ada kontak') AS kontak
FROM pelanggan
ORDER BY nama;

-- pengganti angka: NULL poin dibaca sebagai nol
SELECT nama,
       COALESCE(poin, 0) AS poin_aman,
       COALESCE(poin, 0) * 2 AS poin_ganda
FROM pelanggan
ORDER BY nama;

greatest-least.sql

Unduh berkas
sql
-- GREATEST dan LEAST mengabaikan NULL di dalam daftar
SELECT GREATEST(3, NULL, 7) AS terbesar,
       LEAST('b', NULL, 'a') AS terkecil;

-- jika seluruh argumen NULL, hasilnya NULL
SELECT GREATEST(NULL, NULL) IS NULL AS semua_null;

-- di konteks tabel: NULL pada poin tidak menular ke hasil
SELECT nama,
       poin,
       GREATEST(poin, 50) AS poin_minimal_50,
       LEAST(poin, 100) AS poin_maksimal_100
FROM pelanggan
ORDER BY nama;

nullif.sql

Unduh berkas
sql
-- NULLIF(a, b): menghasilkan NULL ketika a sama dengan b
SELECT NULLIF('sama', 'sama') AS hasil_null,
       NULLIF('beda', 'sama') AS hasil_tetap;

-- pola klasik: melindungi pembagian dari pembagi nol
SELECT 100 / NULLIF(0, 0) AS dibagi_nullif;

-- normalisasi string kosong menjadi NULL
SELECT NULLIF('', '') AS kosong_jadi_null,
       NULLIF('Budi', '') AS teks_normal;

-- di konteks tabel: string kosong telepon dinormalisasi menjadi NULL
SELECT nama,
       telepon,
       NULLIF(telepon, '') AS telepon_bersih
FROM pelanggan
ORDER BY nama;