Enum membatasi nilai ke daftar tertutup, tetapi banyak aturan bisnis tidak berbentuk daftar: kode pos harus tepat lima digit, harga tidak boleh negatif, username hanya boleh huruf kecil dan angka. Aturan seperti ini biasa ditulis sebagai CHECK constraint — dan biasa pula disalin-tempel ke setiap tabel sampai satu-dua tabel terlewat. PostgreSQL menawarkan domain: tipe dasar yang sudah ada (text, numeric) dibungkus dengan aturan validasi bernama, lalu dipakai sebagai tipe kolom di mana saja. Mengubah aturannya berarti mengubah satu definisi, bukan dua puluh tabel.
Domain: Tipe Dasar Plus Aturan #
Domain dibuat dengan CREATE DOMAIN, menyebut tipe dasar dan CHECK yang memakai kata kunci VALUE sebagai placeholder nilai yang diuji:
-- domain: tipe dasar + aturan validasi bernama, sekali definisi dipakai di mana saja
CREATE DOMAIN kode_pos AS text
CHECK (VALUE ~ '^[0-9]{5}$');
CREATE TABLE cabang (
id integer PRIMARY KEY,
nama text NOT NULL,
kota text NOT NULL,
kode_pos kode_pos
);
INSERT INTO cabang (id, nama, kota, kode_pos) VALUES
(1, 'Toko Maju Jaya Pusat', 'Bandung', '40115'),
(2, 'Cabang Surabaya', 'Surabaya', '60111'),
(3, 'Cabang Bogor', 'Bogor', '16153');
SELECT id, nama, kota, kode_pos
FROM cabang
ORDER BY id;Output yang Dihasilkan:
CREATE DOMAIN
CREATE TABLE
INSERT 0 3
id | nama | kota | kode_pos
----+----------------------+----------+----------
1 | Toko Maju Jaya Pusat | Bandung | 40115
2 | Cabang Surabaya | Surabaya | 60111
3 | Cabang Bogor | Bogor | 16153
(3 rows)CREATE DOMAIN kode_pos AS text CHECK (VALUE ~ '^[0-9]{5}$') membungkus tipe text dengan regex lima digit. Kolom kode_pos pada tabel cabang lalu ditulis seperti kolom biasa — tetapi setiap nilai yang masuk atau diperbarui kini melewati pemeriksaan itu. Tiga cabang dengan kode pos sah lolos tanpa gesekan.
Pelanggaran Constraint Domain #
Nilai yang melanggar aturan domain ditolak saat INSERT atau UPDATE — dengan pesan error yang menyebut nama domain, bukan nama tabel:
-- nilai yang gagal CHECK domain ditolak dengan nama constraint domain
INSERT INTO cabang (id, nama, kota, kode_pos) VALUES
(4, 'Cabang Palsu', 'Bogor', 'BOGOR1');Output yang Dihasilkan:
psql:kode-pos-error.sql:3: ERROR: value for domain kode_pos violates check constraint "kode_pos_check"'BOGOR1' tidak cocok dengan pola lima digit, dan PostgreSQL menolak INSERT dengan menyebut check constraint "kode_pos_check" — nama otomatis yang diturunkan dari nama domain. Karena penolakan terjadi di lapisan tipe, tidak ada tabel pengguna domain yang bisa lolos: peringatan ini sama untuk cabang, untuk tabel pelanggan, untuk tabel pemasok, semuanya.
Jebakan NULL: CHECK Domain Tidak Menguji NULL #
Perilaku paling mengejutkan domain — dan CHECK pada umumnya — adalah perlakuannya terhadap NULL. CHECK mengevaluasi VALUE ~ '...' dan hasil NULL (tidak diketahui) dianggap lolos:
-- JEBAKAN: CHECK domain tidak pernah menguji NULL
INSERT INTO cabang (id, nama, kota, kode_pos) VALUES
(4, 'Cabang Sementara', 'Depok', NULL);
SELECT id, nama, kota, kode_pos
FROM cabang
ORDER BY id;
-- NOT NULL ditulis pada definisi domain bila NULL memang dilarang
CREATE DOMAIN username AS text NOT NULL
CHECK (VALUE ~ '^[a-z0-9_]+$');
CREATE TABLE akun (
id integer PRIMARY KEY,
pemilik username
);
INSERT INTO akun (id, pemilik) VALUES
(1, 'budi_santoso'),
(2, 'siti_melati');
SELECT id, pemilik
FROM akun
ORDER BY id;Output yang Dihasilkan:
INSERT 0 1
id | nama | kota | kode_pos
----+----------------------+----------+----------
1 | Toko Maju Jaya Pusat | Bandung | 40115
2 | Cabang Surabaya | Surabaya | 60111
3 | Cabang Bogor | Bogor | 16153
4 | Cabang Sementara | Depok |
(4 rows)
CREATE DOMAIN
CREATE TABLE
INSERT 0 2
id | pemilik
----+--------------
1 | budi_santoso
2 | siti_melati
(2 rows)INSERT keempat menyimpan NULL pada kolom kode_pos — dan diterima, meski regex jelas tidak cocok dengan NULL. Inilah warisan semantik SQL standar: CHECK hanya menolak bila hasil pengujian pasti false. Bila NULL memang dilarang, NOT NULL harus ditulis eksplisit pada definisi domain, seperti pada domain username di file — dan percobaan memasukkan NULL ke kolom bertipe domain itu akan ditolak dengan pesan domain username does not allow null values.
Domain di Atas numeric #
Domain tidak terbatas pada text. Di atas numeric dengan typmod, domain membawa dua aturan sekaligus: pembulatan skala dari tipe dasar dan CHECK milik domain:
-- domain di atas numeric dengan typmod: pembulatan skala + CHECK berlaku bersamaan
CREATE DOMAIN harga_rp AS numeric(12, 2)
CHECK (VALUE >= 0);
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
harga harga_rp NOT NULL
);
INSERT INTO produk (id, nama, harga) VALUES
(1, 'Buku Tulis 384 Halaman', 9500),
(2, 'Laptop Kerja 14 inci', 8500000),
(3, 'Mouse Wireless', 185000.755);
-- skala numeric(12,2) membulatkan nilai di atas dua desimal
SELECT id, nama, harga
FROM produk
ORDER BY id;
-- pembaruan juga melewati pemeriksaan domain yang sama
UPDATE produk SET harga = harga * 1.1
WHERE id = 1;
SELECT id, nama, harga
FROM produk
WHERE id = 1;Output yang Dihasilkan:
CREATE DOMAIN
CREATE TABLE
INSERT 0 3
id | nama | harga
----+------------------------+------------
1 | Buku Tulis 384 Halaman | 9500.00
2 | Laptop Kerja 14 inci | 8500000.00
3 | Mouse Wireless | 185000.76
(3 rows)
UPDATE 1
id | nama | harga
----+------------------------+----------
1 | Buku Tulis 384 Halaman | 10450.00
(1 row)Domain harga_rp dideklarasikan sebagai numeric(12,2) dengan CHECK (VALUE >= 0) — harga rupiah dua desimal yang tak boleh negatif. Mouse Wireless di-INSERT dengan tiga desimal (185000.755) dan tersimpan sebagai 185000.76: pembulatan skala bekerja persis seperti numeric(12,2) polos, lalu CHECK menguji hasilnya. UPDATE harga * 1.1 juga melewati jalur yang sama — nilai baru dihitung, dibulatkan, diuji, baru disimpan. INSERT dengan nilai negatif akan ditolak dengan mekanisme yang sama seperti BOGOR1 di bagian sebelumnya.
Evolusi Domain: NOT VALID dan VALIDATE #
Aturan domain bisa ditambahkan setelah data tersimpan — dan di sinilah domain menunjukkan jangkauannya. Kita tambahkan aturan baru “harga wajar maksimal lima juta” ke domain harga_rp yang sudah dipakai dua tabel, dengan satu baris lawas yang melanggar:
-- baris lama (9 juta) melanggar aturan baru (maksimal 5 juta)
CREATE TABLE stok_lama (
id integer PRIMARY KEY,
nama text NOT NULL,
harga harga_rp NOT NULL
);
INSERT INTO stok_lama (id, nama, harga) VALUES
(1, 'Laptop Mewah Lawas', 9000000),
(2, 'Buku Tulis 384 Halaman', 9500);
ALTER DOMAIN harga_rp ADD CONSTRAINT harga_wajar
CHECK (VALUE <= 5000000);Output yang Dihasilkan:
psql:alter-domain-error.sql:13: ERROR: column "harga" of table "produk" contains values that violate the new constraintALTER DOMAIN ... ADD CONSTRAINT tanpa syarat apa pun langsung memeriksa seluruh data yang ada — dan menemukan pelanggar. Perhatikan pesannya: yang disebut adalah kolom harga milik tabel produk (Laptop 8,5 juta), padahal baris lawas 9 juta ada di stok_lama — server memeriksa semua tabel pengguna domain dan melapor pelanggar pertama yang ditemukannya. Penambahan constraint pun gagal total. Jalur yang benar untuk data lawas adalah NOT VALID:
-- NOT VALID: pasang constraint tanpa memeriksa baris lama
ALTER DOMAIN harga_rp ADD CONSTRAINT harga_wajar
CHECK (VALUE <= 5000000) NOT VALID;
\dD harga_rp
-- constraint domain berlaku untuk SEMUA tabel pemakai domain:
-- perbaiki setiap baris pelanggar sebelum memvalidasi
UPDATE stok_lama SET harga = 4500000 WHERE id = 1;
UPDATE produk SET harga = 4500000 WHERE id = 2;
-- VALIDATE memeriksa ulang seluruh data dan menutup proses
ALTER DOMAIN harga_rp VALIDATE CONSTRAINT harga_wajar;
\dD harga_rp
-- DROP CONSTRAINT melepas aturan sepenuhnya dari domain
ALTER DOMAIN harga_rp DROP CONSTRAINT harga_wajar;
\dD harga_rpOutput yang Dihasilkan:
ALTER DOMAIN
List of domains
Schema | Name | Type | Collation | Nullable | Default | Check
--------+----------+---------------+-----------+----------+---------+-------------------------------------------------------------------------
public | harga_rp | numeric(12,2) | | | | CHECK (VALUE >= 0::numeric) CHECK (VALUE <= 5000000::numeric) NOT VALID
(1 row)
UPDATE 1
UPDATE 1
ALTER DOMAIN
List of domains
Schema | Name | Type | Collation | Nullable | Default | Check
--------+----------+---------------+-----------+----------+---------+---------------------------------------------------------------
public | harga_rp | numeric(12,2) | | | | CHECK (VALUE >= 0::numeric) CHECK (VALUE <= 5000000::numeric)
(1 row)
ALTER DOMAIN
List of domains
Schema | Name | Type | Collation | Nullable | Default | Check
--------+----------+---------------+-----------+----------+---------+-----------------------------
public | harga_rp | numeric(12,2) | | | | CHECK (VALUE >= 0::numeric)
(1 row)Urutan peristiwanya: ADD CONSTRAINT ... NOT VALID memasang aturan tanpa memeriksa baris lama — \dD pertama masih menampilkan penanda NOT VALID pada daftar check. Dua UPDATE kemudian membersihkan kedua pelanggar di dua tabel (Laptop Mewah Lawas dan Laptop Kerja, keduanya turun ke 4,5 juta). VALIDATE CONSTRAINT memeriksa ulang seluruh data; karena sudah bersih, validasi berhasil dan penanda NOT VALID hilang dari \dD kedua. DROP CONSTRAINT menutup file: aturan harga_wajar dilepas sepenuhnya dan \dD ketiga hanya menyisakan CHECK bawaan domain.
Catatan penting selama periode NOT VALID: aturan baru tetap menolak tulisan baru. INSERT atau UPDATE yang mencoba menyimpan 6 juta ke kolom harga_rp saat itu akan gagal dengan value for domain harga_rp violates check constraint "harga_wajar" — NOT VALID hanya memaafkan data lama, bukan data yang baru datang.
Domain vs CHECK Constraint per Tabel #
Kapan domain mengalahkan CHECK biasa? Semua kolom yang memakai domain membaca aturan dari satu tempat yang sama:
| Aspek | CHECK di tabel | Domain |
|---|---|---|
| Sekali definisi, banyak tabel | Tidak — constraint dideklarasikan per tabel (dan disalin-tempel) | Ya — satu nama tipe dipakai semua kolom |
| Mengubah aturan | ALTER TABLE di tiap tabel, satu per satu | ALTER DOMAIN sekali, kena ke semua kolom |
| Nama di pesan error | nama constraint milik tabel | nama constraint domain, mudah dilacak |
| Typmod tipe dasar | ditulis ulang di tiap kolom | terbawa dalam definisi domain |
| NULL lolos dari CHECK | Ya | Ya — kecuali NOT NULL ditulis di domain |
| Paling cocok untuk | aturan khusus satu tabel | aturan lintas tabel: kode pos, harga, username, email |
Pembahasan penuh CHECK constraint sendiri ada di chapter CHECK Constraint . Aturan praktisnya ringkas: aturan yang hanya berarti untuk satu tabel tetaplah CHECK di tabel itu; aturan yang menyatakan “nilai seperti INI selalu sah/begini bentuknya” di mana pun ia muncul — itulah wilayah domain.
Inspeksi: \dD dan Katalog #
psql menyediakan \dD khusus untuk mendaftar domain, dan katalog sistem menyimpan domain sebagai tipe dengan penanda typtype = 'd':
-- \dD mendaftar seluruh domain beserta check-nya
\dD
-- katalog: domain adalah tipe dengan typtype 'd' di atas tipe dasar
SELECT typname AS domain,
format_type(typbasetype, typtypmod) AS tipe_dasar
FROM pg_type
WHERE typtype = 'd'
AND typnamespace = 'public'::regnamespace
ORDER BY typname;Output yang Dihasilkan:
List of domains
Schema | Name | Type | Collation | Nullable | Default | Check
--------+----------+---------------+-----------+----------+---------+--------------------------------------
public | harga_rp | numeric(12,2) | | | | CHECK (VALUE >= 0::numeric)
public | kode_pos | text | | | | CHECK (VALUE ~ '^[0-9]{5}$'::text)
public | username | text | | not null | | CHECK (VALUE ~ '^[a-z0-9_]+$'::text)
(3 rows)
domain | tipe_dasar
----------+---------------
harga_rp | numeric(12,2)
kode_pos | text
username | text
(3 rows)\dD menampilkan semua domain di database beserta tipe dasar, status nullable, dan daftar check-nya. Query katalog menyarikan hal yang sama: domain kode_pos, username, dan harga_rp semuanya bertipe dasar tertentu — perhatikan harga_rp yang melaporkan numeric(12,2) penuh dengan typmodnya, bukan sekadar numeric.
Domain memvalidasi satu nilai saat ditulis. Tipe kustom terakhir section ini memvalidasi pasangan batas: rentang tanggal kapan tarif berlaku, rentang jam kapan shift berjalan, rentang angka kapan diskon aktif — dengan operator overlap dan containment yang siap dipakai untuk menjawab “apakah dua rentang bertabrakan?”.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
alter-domain-error.sql
Unduh berkas-- baris lama (9 juta) melanggar aturan baru (maksimal 5 juta)
CREATE TABLE stok_lama (
id integer PRIMARY KEY,
nama text NOT NULL,
harga harga_rp NOT NULL
);
INSERT INTO stok_lama (id, nama, harga) VALUES
(1, 'Laptop Mewah Lawas', 9000000),
(2, 'Buku Tulis 384 Halaman', 9500);
ALTER DOMAIN harga_rp ADD CONSTRAINT harga_wajar
CHECK (VALUE <= 5000000);
alter-domain.sql
Unduh berkas-- NOT VALID: pasang constraint tanpa memeriksa baris lama
ALTER DOMAIN harga_rp ADD CONSTRAINT harga_wajar
CHECK (VALUE <= 5000000) NOT VALID;
\dD harga_rp
-- constraint domain berlaku untuk SEMUA tabel pemakai domain:
-- perbaiki setiap baris pelanggar sebelum memvalidasi
UPDATE stok_lama SET harga = 4500000 WHERE id = 1;
UPDATE produk SET harga = 4500000 WHERE id = 2;
-- VALIDATE memeriksa ulang seluruh data dan menutup proses
ALTER DOMAIN harga_rp VALIDATE CONSTRAINT harga_wajar;
\dD harga_rp
-- DROP CONSTRAINT melepas aturan sepenuhnya dari domain
ALTER DOMAIN harga_rp DROP CONSTRAINT harga_wajar;
\dD harga_rp
buat-domain.sql
Unduh berkas-- domain: tipe dasar + aturan validasi bernama, sekali definisi dipakai di mana saja
CREATE DOMAIN kode_pos AS text
CHECK (VALUE ~ '^[0-9]{5}$');
CREATE TABLE cabang (
id integer PRIMARY KEY,
nama text NOT NULL,
kota text NOT NULL,
kode_pos kode_pos
);
INSERT INTO cabang (id, nama, kota, kode_pos) VALUES
(1, 'Toko Maju Jaya Pusat', 'Bandung', '40115'),
(2, 'Cabang Surabaya', 'Surabaya', '60111'),
(3, 'Cabang Bogor', 'Bogor', '16153');
SELECT id, nama, kota, kode_pos
FROM cabang
ORDER BY id;
domain-null.sql
Unduh berkas-- JEBAKAN: CHECK domain tidak pernah menguji NULL
INSERT INTO cabang (id, nama, kota, kode_pos) VALUES
(4, 'Cabang Sementara', 'Depok', NULL);
SELECT id, nama, kota, kode_pos
FROM cabang
ORDER BY id;
-- NOT NULL ditulis pada definisi domain bila NULL memang dilarang
CREATE DOMAIN username AS text NOT NULL
CHECK (VALUE ~ '^[a-z0-9_]+$');
CREATE TABLE akun (
id integer PRIMARY KEY,
pemilik username
);
INSERT INTO akun (id, pemilik) VALUES
(1, 'budi_santoso'),
(2, 'siti_melati');
SELECT id, pemilik
FROM akun
ORDER BY id;
domain-numeric.sql
Unduh berkas-- domain di atas numeric dengan typmod: pembulatan skala + CHECK berlaku bersamaan
CREATE DOMAIN harga_rp AS numeric(12, 2)
CHECK (VALUE >= 0);
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
harga harga_rp NOT NULL
);
INSERT INTO produk (id, nama, harga) VALUES
(1, 'Buku Tulis 384 Halaman', 9500),
(2, 'Laptop Kerja 14 inci', 8500000),
(3, 'Mouse Wireless', 185000.755);
-- skala numeric(12,2) membulatkan nilai di atas dua desimal
SELECT id, nama, harga
FROM produk
ORDER BY id;
-- pembaruan juga melewati pemeriksaan domain yang sama
UPDATE produk SET harga = harga * 1.1
WHERE id = 1;
SELECT id, nama, harga
FROM produk
WHERE id = 1;
katalog-domain.sql
Unduh berkas-- \dD mendaftar seluruh domain beserta check-nya
\dD
-- katalog: domain adalah tipe dengan typtype 'd' di atas tipe dasar
SELECT typname AS domain,
format_type(typbasetype, typtypmod) AS tipe_dasar
FROM pg_type
WHERE typtype = 'd'
AND typnamespace = 'public'::regnamespace
ORDER BY typname;
kode-pos-error.sql
Unduh berkas-- nilai yang gagal CHECK domain ditolak dengan nama constraint domain
INSERT INTO cabang (id, nama, kota, kode_pos) VALUES
(4, 'Cabang Palsu', 'Bogor', 'BOGOR1');