Bug data yang paling memakan waktu biasanya bukan nilai yang salah, melainkan nilai yang tidak ada: nomor resi yang kosong saat kurir hendak melacak paket, berat pengiriman yang NULL sehingga ongkir gagal dihitung, status pelanggan yang tidak diketahui sehingga laporan keuangan salah potong. Constraint NOT NULL adalah garis pertahanan paling murah sekaligus paling efektif terhadap kelas bug ini: satu kata di definisi kolom, dan PostgreSQL menolak setiap baris yang mencoba menyimpan nilai kosong di sana. Chapter ini menguji perilakunya, membahas kapan NULL justru pilihan yang tepat, lalu membahas cara memasang constraint ini pada tabel yang sudah berisi data — termasuk saat penempelan itu ditolak.
Perilaku Dasar NOT NULL #
Constraint NOT NULL dideklarasikan langsung pada kolom yang nilainya wajib ada. Tabel pengiriman berikut menandai empat kolom sebagai wajib dan membiarkan satu kolom benar-benar opsional:
-- Kolom NOT NULL wajib terisi; telepon_penerima memang boleh kosong
CREATE TABLE pengiriman (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nomor_resi text NOT NULL,
kota_tujuan text NOT NULL,
berat_gram integer NOT NULL,
telepon_penerima text
);
INSERT INTO pengiriman (nomor_resi, kota_tujuan, berat_gram, telepon_penerima) VALUES
('RESI-0001', 'Bandung', 1200, '0812-0000-0001'),
('RESI-0002', 'Surabaya', 850, NULL);
SELECT id, nomor_resi, kota_tujuan, berat_gram, telepon_penerima
FROM pengiriman
ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 2
id | nomor_resi | kota_tujuan | berat_gram | telepon_penerima
----+------------+-------------+------------+------------------
1 | RESI-0001 | Bandung | 1200 | 0812-0000-0001
2 | RESI-0002 | Surabaya | 850 |
(2 rows)Dua baris berhasil masuk — termasuk baris kedua yang kolom telepon_penerima-nya NULL, karena kolom tersebut memang tidak memakai NOT NULL. Desain ini menyatakan niatnya dengan jelas: setiap pengiriman wajib punya nomor resi, kota tujuan, dan berat; nomor telepon penerima kadang memang tidak diketahui.
Dua Cara Gagal: Kolom Dihilangkan dan NULL Eksplisit #
Pelanggaran NOT NULL datang lewat dua pintu. Pintu pertama: kolom tidak disebutkan dalam INSERT — dan kolom itu tidak punya DEFAULT:
INSERT INTO pengiriman (nomor_resi, kota_tujuan)
VALUES ('RESI-0003', 'Medan');Output yang Dihasilkan:
psql:nn-tanpa-nilai.sql:2: ERROR: null value in column "berat_gram" of relation "pengiriman" violates not-null constraint
DETAIL: Failing row contains (3, RESI-0003, Medan, null, null).Nilai bawaan untuk kolom yang tidak disebutkan adalah NULL; tanpa DEFAULT yang mengubahnya, PostgreSQL menolak baris tersebut. Pintu kedua: NULL yang dituliskan secara eksplisit:
INSERT INTO pengiriman (nomor_resi, kota_tujuan, berat_gram)
VALUES (NULL, 'Medan', 600);Output yang Dihasilkan:
psql:nn-null-eksplisit.sql:2: ERROR: null value in column "nomor_resi" of relation "pengiriman" violates not-null constraint
DETAIL: Failing row contains (4, null, Medan, 600, null).Kedua error berbagi format yang informatif: nama kolom yang dilanggar, dan DETAIL: Failing row contains (...) yang memperlihatkan seluruh baris gagal — catat angka id yang terpakai dalam DETAIL: sequence identity tetap maju walau barisnya ditolak. Error NOT NULL adalah error yang paling murah diperbaiki: pemicunya terlihat persis di pesannya.
Kapan NULL Memang Tepat #
Menempel NOT NULL di semua kolom terasa disiplin, tetapi NULL bukan musuh — NULL adalah cara database berkata “nilai ini tidak tersedia”. Dua makna yang berbeda perlu dibedakan saat merancang:
| Situasi | Nilai yang tepat | Alasan |
|---|---|---|
| Nomor resi belum diterbitkan | NULL (bukan '') | string kosong adalah nilai yang mengaku ada |
| Telepon pelanggan belum dikonfirmasi | NULL | 0 atau '-' adalah data palsu |
| Harga produk memang belum ditentukan | NULL | 0 adalah harga sungguhan yang salah dibaca laporan |
| Tanggal akhir kontrak yang masih berjalan | NULL | tidak ada tanggal yang mewakili “belum berakhir” |
| Kolom yang selalu tersedia di proses bisnis | NOT NULL | tidak ada alasan data bisa kosong |
Aturan praktisnya: mulai dari NOT NULL untuk kolom yang proses bisnisnya selalu menghasilkan nilai (nama, waktu kejadian, jumlah), lalu izinkan NULL hanya untuk kolom yang punya alasan bisnis untuk kosong — dan pastikan aplikasi sadar menangani kasus itu.
Memasang NOT NULL pada Tabel yang Sudah Berisi Data #
Tabel lama kerap dipasangi NOT NULL belakangan lewat ALTER TABLE ... SET NOT NULL. Kendalanya: data yang sudah tersimpan harus dibuktikan tidak memuat NULL dulu:
ALTER TABLE pengiriman ALTER COLUMN telepon_penerima SET NOT NULL;Output yang Dihasilkan:
psql:nn-set-not-null-gagal.sql:1: ERROR: column "telepon_penerima" of relation "pengiriman" contains null valuesPesan column "telepon_penerima" of relation "pengiriman" contains null values menjelaskan semuanya: baris RESI-0002 dari contoh pertama masih menyimpan NULL, dan PostgreSQL menolak memasang constraint yang langsung terlanggar. Jalannya dua langkah — isi data yang kosong, lalu pasang constraint:
-- Lengkapi data yang kosong dulu, baru pasang constraint
UPDATE pengiriman
SET telepon_penerima = 'belum dikonfirmasi'
WHERE telepon_penerima IS NULL;
ALTER TABLE pengiriman ALTER COLUMN telepon_penerima SET NOT NULL;
\d pengirimanOutput yang Dihasilkan:
UPDATE 1
ALTER TABLE
Table "public.pengiriman"
Column | Type | Collation | Nullable | Default
------------------+---------+-----------+----------+------------------------------
id | bigint | | not null | generated always as identity
nomor_resi | text | | not null |
kota_tujuan | text | | not null |
berat_gram | integer | | not null |
telepon_penerima | text | | not null |
Indexes:
"pengiriman_pkey" PRIMARY KEY, btree (id)Setelah UPDATE 1 mengisi nilai pengganti, ALTER TABLE berhasil dan output \d memperlihatkan kelima kolom kini not null. Perlu diketahui: SET NOT NULL memaksa PostgreSQL membaca seluruh tabel untuk memastikan tak ada NULL yang lolos — pada tabel ratusan juta baris ini adalah full table scan yang berkesan, jadi jadwalkan di jam sepi.
Pola NOT NULL + DEFAULT #
Kombinasi NOT NULL DEFAULT <nilai> terlihat kontradiktif tetapi justru sangat produktif: kolom dijamin selalu terisi, sementara pemanggil INSERT tidak wajib menuliskannya:
-- NOT NULL + DEFAULT: kolom wajib terisi, tapi pemanggil tidak wajib menuliskannya
CREATE TABLE pelanggan_baru (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nama text NOT NULL,
status text NOT NULL DEFAULT 'aktif',
poin integer NOT NULL DEFAULT 0
);
INSERT INTO pelanggan_baru (nama) VALUES
('Budi Santoso');
INSERT INTO pelanggan_baru (nama, status, poin) VALUES
('Siti Rahayu', 'premium', 120);
SELECT id, nama, status, poin FROM pelanggan_baru ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 1
INSERT 0 1
id | nama | status | poin
----+--------------+---------+------
1 | Budi Santoso | aktif | 0
2 | Siti Rahayu | premium | 120
(2 rows)Insert pertama tidak menyebut status maupun poin, dan keduanya terisi 'aktif' dan 0 — bukan NULL — sehingga constraint terpenuhi tanpa usaha pemanggil. Insert kedua menimpa nilai bawaan secara eksplisit. Perlu dicatat: DEFAULT hanya mengisi kolom yang tidak disebut; menuliskan NULL secara eksplisit tetap ditolak dengan error yang sama seperti dua demo sebelumnya.
Pola ini pas untuk kolom status, pembuatan timestamp, dan nilai awal hitungan — nanti di chapter Default Value pola ini diperdalam sampai DEFAULT berbasis ekspresi. Satu batas NOT NULL tetap berlaku: ia hanya memastikan nilai ada, bukan nilai itu masuk akal. Status 'aktif' bisa saja tertukar menjadi 'aktiff' dan lolos. Memvalidasi isi nilai — bukan sekadar keberadaannya — adalah tugas constraint berikutnya: CHECK.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
nn-dasar.sql
Unduh berkas-- Kolom NOT NULL wajib terisi; telepon_penerima memang boleh kosong
CREATE TABLE pengiriman (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nomor_resi text NOT NULL,
kota_tujuan text NOT NULL,
berat_gram integer NOT NULL,
telepon_penerima text
);
INSERT INTO pengiriman (nomor_resi, kota_tujuan, berat_gram, telepon_penerima) VALUES
('RESI-0001', 'Bandung', 1200, '0812-0000-0001'),
('RESI-0002', 'Surabaya', 850, NULL);
SELECT id, nomor_resi, kota_tujuan, berat_gram, telepon_penerima
FROM pengiriman
ORDER BY id;
nn-default.sql
Unduh berkas-- NOT NULL + DEFAULT: kolom wajib terisi, tapi pemanggil tidak wajib menuliskannya
CREATE TABLE pelanggan_baru (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nama text NOT NULL,
status text NOT NULL DEFAULT 'aktif',
poin integer NOT NULL DEFAULT 0
);
INSERT INTO pelanggan_baru (nama) VALUES
('Budi Santoso');
INSERT INTO pelanggan_baru (nama, status, poin) VALUES
('Siti Rahayu', 'premium', 120);
SELECT id, nama, status, poin FROM pelanggan_baru ORDER BY id;
nn-null-eksplisit.sql
Unduh berkasINSERT INTO pengiriman (nomor_resi, kota_tujuan, berat_gram)
VALUES (NULL, 'Medan', 600);
nn-set-not-null-berhasil.sql
Unduh berkas-- Lengkapi data yang kosong dulu, baru pasang constraint
UPDATE pengiriman
SET telepon_penerima = 'belum dikonfirmasi'
WHERE telepon_penerima IS NULL;
ALTER TABLE pengiriman ALTER COLUMN telepon_penerima SET NOT NULL;
\d pengiriman
nn-set-not-null-gagal.sql
Unduh berkasALTER TABLE pengiriman ALTER COLUMN telepon_penerima SET NOT NULL;
nn-tanpa-nilai.sql
Unduh berkasINSERT INTO pengiriman (nomor_resi, kota_tujuan)
VALUES ('RESI-0003', 'Medan');