Banyak kolom di dunia nyata hanya boleh berisi nilai dari daftar tetap: status pesanan (baru, diproses, dikirim, selesai), golongan darah, hari kerja. Kalau kolom seperti itu dideklarasikan sebagai text, tidak ada yang mencegah 'diprose', 'Diproses', atau 'sudah jadi' masuk — dan aplikasi harus memvalidasi sendiri di setiap titik penulisan. Tipe ENUM PostgreSQL menutup celah itu: kita mendefinisikan sendiri sebuah tipe berisi daftar nilai resmi, lalu server menolak nilai lain apa pun yang tidak terdaftar. Nilai ENUM juga membawa urutan bawaan dan tersimpan ringkas, dua keuntungan yang tidak dimiliki kolom text dengan CHECK. Chapter ini membangun tipe status_pesanan dari nol, membuktikan perilaku urutannya, lalu menutup dengan panduan kapan memilih ENUM dibanding dua alternatifnya.
Membuat Tipe ENUM #
Berbeda dari tipe bawaan, ENUM dibuat sebagai tipe khusus tingkat schema lewat CREATE TYPE, terpisah dari tabel mana pun yang memakainya — satu tipe bisa dipakai banyak kolom sekaligus:
-- ENUM dibuat sebagai tipe tersendiri lewat CREATE TYPE,
-- bukan bagian dari definisi tabel
CREATE TYPE status_pesanan AS ENUM ('baru', 'diproses', 'dikirim', 'selesai');
-- enum_range() mengembalikan seluruh nilai yang terdaftar,
-- berurut sesuai urutan deklarasinya
SELECT enum_range(NULL::status_pesanan) AS daftar_status;Output yang Dihasilkan:
CREATE TYPE
daftar_status
---------------------------------
{baru,diproses,dikirim,selesai}
(1 row)Fungsi enum_range(NULL::status_pesanan) mengembalikan seluruh nilai tipe tersebut sebagai array — output {baru,diproses,dikirim,selesai} menegaskan urutannya persis mengikuti urutan penulisan di CREATE TYPE. Urutan ini bukan detail kosmetik: seperti akan kita buktikan sebentar lagi, urutan deklarasi inilah yang menjadi dasar ORDER BY dan pembanding seperti < dan >=.
Tabel dengan Kolom ENUM #
Setelah tipenya ada, kolom tabel tinggal memakai nama tipe itu seperti tipe biasa:
CREATE TABLE pesanan (
id integer PRIMARY KEY,
pelanggan text NOT NULL,
status status_pesanan NOT NULL DEFAULT 'baru'
);
INSERT INTO pesanan (id, pelanggan, status) VALUES
(1, 'Budi', 'baru'),
(2, 'Siti', 'dikirim'),
(3, 'Ani', 'selesai'),
(4, 'Rina', 'baru'),
(5, 'Dewi', 'diproses');
SELECT id, pelanggan, status
FROM pesanan
ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 5
id | pelanggan | status
----+-----------+----------
1 | Budi | baru
2 | Siti | dikirim
3 | Ani | selesai
4 | Rina | baru
5 | Dewi | diproses
(5 rows)Definisi kolomnya status status_pesanan NOT NULL DEFAULT 'baru' — sama ergonomic-nya dengan kolom text, lengkap dengan nilai default. Nilai literal ditulis sebagai string biasa; saat INSERT dijalankan, PostgreSQL memeriksa tiap string terhadap daftar nilai tipe dan langsung menolak yang tidak terdaftar. Lima baris di atas sengaja diacak urutan statusnya supaya perilaku pengurutan ENUM terlihat jelas di bagian berikutnya.
Urutan Mengikuti Urutan Deklarasi #
Inilah fitur yang membedakan ENUM dari sekadar text dengan CHECK: nilai-nilainya punya urutan. Secara internal setiap nilai disimpan sebagai angka kecil sesuai posisinya di daftar, sehingga pengurutan dan pembanding bekerja atas urutan deklarasi — bukan urutan alfabetis:
-- ORDER BY pada kolom enum memakai urutan deklarasi tipe:
-- baru < diproses < dikirim < selesai
SELECT id, pelanggan, status
FROM pesanan
ORDER BY status, id;
-- pembanding juga mengikuti urutan yang sama
SELECT 'baru'::status_pesanan < 'selesai'::status_pesanan AS lebih_awal;
-- filter nilai tunggal, sama seperti kolom text
SELECT id, pelanggan
FROM pesanan
WHERE status = 'diproses'
ORDER BY id;
-- rentang nilai memanfaatkan urutan deklarasi
SELECT id, pelanggan, status
FROM pesanan
WHERE status >= 'diproses'
ORDER BY status, id;Output yang Dihasilkan:
id | pelanggan | status
----+-----------+----------
1 | Budi | baru
4 | Rina | baru
5 | Dewi | diproses
2 | Siti | dikirim
3 | Ani | selesai
(5 rows)
lebih_awal
------------
t
(1 row)
id | pelanggan
----+-----------
5 | Dewi
(1 row)
id | pelanggan | status
----+-----------+----------
5 | Dewi | diproses
2 | Siti | dikirim
3 | Ani | selesai
(3 rows)Perhatikan hasil query pertama: seluruh pesanan tertata baru → diproses → dikirim → selesai meskipun baris-barisnya diacak saat disisipkan. Urutan ini berasal dari deklarasi tipe, bukan dari urutan alfabetis — buktinya ada pada pasangan diproses dan dikirim: secara alfabetis dikirim lebih dulu, tetapi hasil query menempatkan diproses di atasnya karena diproses dideklarasikan lebih awal. Pembanding 'baru' < 'selesai' bernilai t karena baru dideklarasikan lebih dulu. Filter rentang status >= 'diproses' mengembalikan tiga baris berstatus diproses, dikirim, dan selesai — pola yang menyusun “pesanan yang sudah lewat tahap baru” tanpa menuliskan semua nilai satu per satu.
Nilai di Luar Daftar Ditolak #
Sekarang sisi penegakan aturannya. Nilai yang tidak terdaftar ditolak server sebelum baris sempat tersimpan:
INSERT INTO pesanan (id, pelanggan, status) VALUES
(6, 'Tono', 'dikembalikan');Output yang Dihasilkan:
psql:enum-invalid.sql:2: ERROR: invalid input value for enum status_pesanan: "dikembalikan"
LINE 2: (6, 'Tono', 'dikembalikan');
^Error invalid input value for enum status_pesanan menyebutkan nilai yang ditolak beserta nama tipenya — pesan yang jauh lebih spesifik daripada pelanggaran CHECK generik. Tipe salah ejaan seperti 'diprose' akan ditolak dengan cara yang sama.
Ada satu jebakan yang hanya muncul lewat parameter aplikasi. Literal 'baru' pada INSERT di atas diterima karena statusnya unknown sehingga langsung ditafsirkan sebagai status_pesanan. Namun begitu nilai itu sudah bertipe text — persis seperti string yang dikirim driver database — server menolaknya karena tidak mau meng casting implisit dari text ke ENUM:
INSERT INTO pesanan (id, pelanggan, status) VALUES
(7, 'Tono', 'baru'::text);Output yang Dihasilkan:
psql:enum-cast.sql:2: ERROR: column "status" is of type status_pesanan but expression is of type text
LINE 2: (7, 'Tono', 'baru'::text);
^
HINT: You will need to rewrite or cast the expression.Error column "status" is of type status_pesanan but expression is of type text disertai HINT: You will need to rewrite or cast the expression. Solusinya eksplisit: tambahkan cast di sisi aplikasi ($1::status_pesanan) atau bungkus di sisi query (CAST($1 AS status_pesanan)).
Menambah Nilai Baru dengan ALTER TYPE #
Daftar nilai bukan barang beku. ALTER TYPE ... ADD VALUE menambahkan anggota baru pada tipe yang sudah dipakai tabel — tanpa menulis ulang tabel yang ada:
-- menambah nilai baru ke tipe yang sudah dipakai tabel
ALTER TYPE status_pesanan ADD VALUE 'dibatalkan';
INSERT INTO pesanan (id, pelanggan, status) VALUES
(8, 'Joko', 'dibatalkan');
SELECT enum_range(NULL::status_pesanan) AS daftar_status;
-- nilai baru selalu berurut SETELAH nilai-nilai lama
SELECT id, pelanggan, status
FROM pesanan
ORDER BY status, id;Output yang Dihasilkan:
ALTER TYPE
INSERT 0 1
daftar_status
--------------------------------------------
{baru,diproses,dikirim,selesai,dibatalkan}
(1 row)
id | pelanggan | status
----+-----------+------------
1 | Budi | baru
4 | Rina | baru
5 | Dewi | diproses
2 | Siti | dikirim
3 | Ani | selesai
8 | Joko | dibatalkan
(6 rows)Nilai dibatalkan kini resmi: enum_range() menampilkannya di akhir daftar, dan baris Joko muncul paling bawah pada ORDER BY status — nilai baru selalu berurut setelah semua nilai lama, apa pun urutan alfabetisnya. Perlu dicatat dua batasan desain ENUM: nilai lama tidak bisa diurutkan ulang maupun dihapus, dan mengganti nama nilainya pun tidak didukung (menghapus nilai harus lewat pembuatan ulang tipe).
ENUM, CHECK, atau Tabel Referensi #
ENUM bukan satu-satunya cara membatasi nilai kolom. Dua pesaingnya — CHECK constraint dan tabel referensi dengan foreign key — punya karakter yang berbeda:
| Pendekatan | Kelebihan | Kekurangan |
|---|---|---|
ENUM | Nilai invalid ditolak server; urutan bawaan; penyimpanan 1–2 byte per nilai | Menambah nilai perlu ALTER TYPE; tidak bisa di-JOIN dengan data lain; nilai tak bisa dihapus |
CHECK constraint | Mudah diubah lewat ALTER TABLE; tetap berupa text sehingga fleksibel | Tidak ada urutan bawaan; daftar nilai tersembunyi di definisi constraint |
| Tabel referensi + FK | Nilai bisa membawa kolom tambahan (label, urutan); bisa di-JOIN, di-audit, di-cache aplikasi | Butuh tabel dan JOIN ekstra; insert nilai baru harus lewat DML |
Pegangan praktisnya: daftar yang stabil dan berurutan alami (golongan darah, hari, status siklus) cocok menjadi ENUM; daftar yang tumbuh mengikuti bisnis dan perlu metadata (kategori produk, tag) lebih pas sebagai tabel referensi; CHECK pas untuk validasi bentuk sederhana (misalnya harga >= 0) ketimbang daftar nilai.
Dengan ENUM, satu kolom kini memegang daftar nilai tertutup. Chapter berikutnya melebarkan satu kolom ke arah yang berlawanan — menampung banyak nilai sekaligus lewat tipe array PostgreSQL.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
alter-add-value.sql
Unduh berkas-- menambah nilai baru ke tipe yang sudah dipakai tabel
ALTER TYPE status_pesanan ADD VALUE 'dibatalkan';
INSERT INTO pesanan (id, pelanggan, status) VALUES
(8, 'Joko', 'dibatalkan');
SELECT enum_range(NULL::status_pesanan) AS daftar_status;
-- nilai baru selalu berurut SETELAH nilai-nilai lama
SELECT id, pelanggan, status
FROM pesanan
ORDER BY status, id;
buat-enum.sql
Unduh berkas-- ENUM dibuat sebagai tipe tersendiri lewat CREATE TYPE,
-- bukan bagian dari definisi tabel
CREATE TYPE status_pesanan AS ENUM ('baru', 'diproses', 'dikirim', 'selesai');
-- enum_range() mengembalikan seluruh nilai yang terdaftar,
-- berurut sesuai urutan deklarasinya
SELECT enum_range(NULL::status_pesanan) AS daftar_status;
enum-cast.sql
Unduh berkasINSERT INTO pesanan (id, pelanggan, status) VALUES
(7, 'Tono', 'baru'::text);
enum-invalid.sql
Unduh berkasINSERT INTO pesanan (id, pelanggan, status) VALUES
(6, 'Tono', 'dikembalikan');
tabel-enum.sql
Unduh berkasCREATE TABLE pesanan (
id integer PRIMARY KEY,
pelanggan text NOT NULL,
status status_pesanan NOT NULL DEFAULT 'baru'
);
INSERT INTO pesanan (id, pelanggan, status) VALUES
(1, 'Budi', 'baru'),
(2, 'Siti', 'dikirim'),
(3, 'Ani', 'selesai'),
(4, 'Rina', 'baru'),
(5, 'Dewi', 'diproses');
SELECT id, pelanggan, status
FROM pesanan
ORDER BY id;
urutan-enum.sql
Unduh berkas-- ORDER BY pada kolom enum memakai urutan deklarasi tipe:
-- baru < diproses < dikirim < selesai
SELECT id, pelanggan, status
FROM pesanan
ORDER BY status, id;
-- pembanding juga mengikuti urutan yang sama
SELECT 'baru'::status_pesanan < 'selesai'::status_pesanan AS lebih_awal;
-- filter nilai tunggal, sama seperti kolom text
SELECT id, pelanggan
FROM pesanan
WHERE status = 'diproses'
ORDER BY id;
-- rentang nilai memanfaatkan urutan deklarasi
SELECT id, pelanggan, status
FROM pesanan
WHERE status >= 'diproses'
ORDER BY status, id;