Chapter Enum di section Tipe Data sudah memperkenalkan CREATE TYPE ... AS ENUM sebagai daftar nilai tertutup. Chapter ini membedah sisi yang belum tersentuh: bagaimana urutan enum mengikuti urutan deklarasi dan mengapa itu berharga untuk laporan pipeline, bagaimana aturan evolusinya bekerja — menambah nilai baru aman di luar transaksi eksplisit tetapi ditolak saat dipakai dalam transaksi yang sama — serta kapan enum kalah dari text + CHECK atau tabel referensi dengan foreign key. Kita pakai satu domain sepanjang chapter: status pesanan Toko Maju Jaya yang mengalir dari menunggu_bayar hingga selesai.

Urutan Mengikuti Deklarasi, Bukan Alfabetis #

Enum tidak sekadar membatasi nilai — enum juga mengurutkan nilainya sesuai posisi saat dideklarasikan. Urutan ini tampak di ORDER BY, di perbandingan, dan di enum_range():

sql buat-enum.sql
-- enum: daftar nilai tertutup dengan urutan mengikuti urutan PENDEKLARASIAN
CREATE TYPE status_pesanan AS ENUM (
    'menunggu_bayar',
    'diproses',
    'dikirim',
    'selesai'
);

CREATE TABLE pesanan (
    id         integer PRIMARY KEY,
    pelanggan  text NOT NULL,
    status     status_pesanan NOT NULL DEFAULT 'menunggu_bayar',
    dibuat     date NOT NULL
);

INSERT INTO pesanan (id, pelanggan, status, dibuat) VALUES
    (1, 'Budi', 'selesai',        DATE '2026-01-05'),
    (2, 'Siti', 'diproses',       DATE '2026-01-06'),
    (3, 'Budi', 'menunggu_bayar', DATE '2026-01-07'),
    (4, 'Dewi', 'dikirim',        DATE '2026-01-08'),
    (5, 'Siti', 'diproses',       DATE '2026-01-09');

-- ORDER BY mengikuti urutan deklarasi, bukan urutan alfabetis
SELECT id, pelanggan, status, dibuat
FROM pesanan
ORDER BY status, id;

Output yang Dihasilkan:

text
CREATE TYPE
CREATE TABLE
INSERT 0 5
 id | pelanggan |     status     |   dibuat   
----+-----------+----------------+------------
  3 | Budi      | menunggu_bayar | 2026-01-07
  2 | Siti      | diproses       | 2026-01-06
  5 | Siti      | diproses       | 2026-01-09
  4 | Dewi      | dikirim        | 2026-01-08
  1 | Budi      | selesai        | 2026-01-05
(5 rows)

Urutan deklarasi sengaja dibuat tidak alfabetis: menunggu_bayar dideklarasikan pertama meski secara alfabetis kalah dari diproses dan dikirim. Hasil ORDER BY status membuktikannya — baris berstatus menunggu_bayar muncul lebih dulu, diikuti diproses, dikirim, terakhir selesai, persis mengikuti alur kerja pesanan dan melawan urutan kamus. Kolom status juga memakai nilai default 'menunggu_bayar', dan tanggal memakai literal tetap agar output stabil.

Perbandingan dan Rentang Nilai #

Karena posisi setiap nilai diketahui, operator pembanding bekerja mulus pada enum — termasuk rentang dengan BETWEEN:

sql urutan-enum.sql
-- perbandingan mengikuti urutan deklarasi: >= 'diproses' berarti diproses ke atas
SELECT id, pelanggan, status
FROM pesanan
WHERE status >= 'diproses'
ORDER BY status, id;

-- rentang nilai: pesanan yang sedang berjalan (diproses s.d. dikirim)
SELECT id, pelanggan, status
FROM pesanan
WHERE status BETWEEN 'diproses' AND 'dikirim'
ORDER BY status, id;

-- enum_range: seluruh nilai yang sah, siap diisi ke dropdown aplikasi
SELECT enum_range(NULL::status_pesanan) AS daftar_status;

-- laporan pipeline: baris per status, terurut mengikuti alur kerja
SELECT status, count(*) AS jumlah
FROM pesanan
GROUP BY status
ORDER BY status;

Output yang Dihasilkan:

text
 id | pelanggan |  status  
----+-----------+----------
  2 | Siti      | diproses
  5 | Siti      | diproses
  4 | Dewi      | dikirim
  1 | Budi      | selesai
(4 rows)

 id | pelanggan |  status  
----+-----------+----------
  2 | Siti      | diproses
  5 | Siti      | diproses
  4 | Dewi      | dikirim
(3 rows)

               daftar_status               
-------------------------------------------
 {menunggu_bayar,diproses,dikirim,selesai}
(1 row)

     status     | jumlah 
----------------+--------
 menunggu_bayar |      1
 diproses       |      2
 dikirim        |      1
 selesai        |      1
(4 rows)

status >= 'diproses' mengembalikan semua pesanan mulai tahap diproses ke atas — perbandingan antar nilai enum, bukan perbandingan teks. BETWEEN 'diproses' AND 'dikirim' memotong rentang “sedang berjalan” dari pipeline. enum_range(NULL::status_pesanan) melaporkan seluruh nilai yang sah sebagai array terurut — bentuk yang pas untuk mengisi dropdown aplikasi tanpa menduplikasi daftar di kode. Query terakhir adalah pola laporan pipeline: GROUP BY status dengan ORDER BY status menghasilkan baris per tahap dalam urutan alur kerja — hal yang mustahil dicapai kolom text tanpa CASE atau tabel pemetaan urutan.

Menambah Nilai Baru: Aman di Luar Transaksi #

Daftar nilai enum bisa bertambah lewat ALTER TYPE ... ADD VALUE, tetapi ada satu aturan versi PostgreSQL 12 ke atas yang wajib dipahami: perintah boleh berjalan di dalam blok transaksi, namun nilai baru tidak boleh dipakai sebelum transaksi itu ter-commit. Mari kita lihat pelanggarannya lebih dulu:

BEGIN;
ALTER TYPE status_pesanan ADD VALUE 'dibatalkan';
SELECT 'dibatalkan'::status_pesanan AS tes_nilai_baru;
COMMIT;

Output yang Dihasilkan:

text
psql:add-value-transaksi.sql:3: ERROR:  unsafe use of new value "dibatalkan" of enum type status_pesanan
LINE 1: SELECT 'dibatalkan'::status_pesanan AS tes_nilai_baru;
               ^
HINT:  New enum values must be committed before they can be used.

ALTER TYPE di baris kedua sukses — error baru muncul saat SELECT mencoba memakai nilai 'dibatalkan' yang belum ter-commit. Pesan unsafe use of new value beserta HINT-nya menjelaskan aturannya lugas: nilai enum baru harus sudah commit sebelum bisa dipakai. Karena error ini menggagalkan seluruh transaksi, penambahan nilai pun batal — state database kembali seperti semula. Sekarang versi yang benar:

-- di luar transaksi eksplisit, ADD VALUE langsung ter-commit dan bisa dipakai
ALTER TYPE status_pesanan ADD VALUE IF NOT EXISTS 'dibatalkan' BEFORE 'selesai';

INSERT INTO pesanan (id, pelanggan, status, dibuat) VALUES
    (6, 'Dewi', 'dibatalkan', DATE '2026-01-10');

SELECT id, pelanggan, status
FROM pesanan
ORDER BY status, id;

SELECT enum_range(NULL::status_pesanan) AS daftar_status;

Output yang Dihasilkan:

text
ALTER TYPE
INSERT 0 1
 id | pelanggan |     status     
----+-----------+----------------
  3 | Budi      | menunggu_bayar
  2 | Siti      | diproses
  5 | Siti      | diproses
  4 | Dewi      | dikirim
  6 | Dewi      | dibatalkan
  1 | Budi      | selesai
(6 rows)

                    daftar_status                     
------------------------------------------------------
 {menunggu_bayar,diproses,dikirim,dibatalkan,selesai}
(1 row)

Di luar blok transaksi eksplisit, psql berjalan mode autocommit: ALTER TYPE ... ADD VALUE ter-commit seketika, sehingga INSERT yang menyimpan 'dibatalkan' langsung sah. Klausa BEFORE 'selesai' menentukan posisi nilai baru — dibatalkan menyisip sebelum selesai, bukan menempel di ujung seperti perilaku bawaan. IF NOT EXISTS melindungi dari penambahan ganda bila skrip dijalankan ulang. Perhatikan kembali output terakhir: enum_range() kini memuat lima nilai dengan dibatalkan di posisi keempat, dan laporan ORDER BY status menempatkan pesanan Dewi tepat sebelum pesanan yang selesai.

Mengganti Nama Nilai dengan RENAME VALUE #

Satu-satunya “sunting” yang diizinkan pada nilai enum adalah mengganti labelnya — fitur sejak PostgreSQL 10 yang menyentuh katalog saja tanpa menulis ulang baris:

-- RENAME VALUE mengganti label tanpa mengubah urutan (PostgreSQL 10+)
ALTER TYPE status_pesanan RENAME VALUE 'menunggu_bayar' TO 'menunggu_pembayaran';

-- data lama otomatis terbaca dengan label barunya
SELECT id, pelanggan, status
FROM pesanan
WHERE status = 'menunggu_pembayaran'
ORDER BY id;

SELECT enum_range(NULL::status_pesanan) AS daftar_status;

Output yang Dihasilkan:

text
ALTER TYPE
 id | pelanggan |       status        
----+-----------+---------------------
  3 | Budi      | menunggu_pembayaran
(1 row)

                       daftar_status                       
-----------------------------------------------------------
 {menunggu_pembayaran,diproses,dikirim,dibatalkan,selesai}
(1 row)

RENAME VALUE 'menunggu_bayar' TO 'menunggu_pembayaran' mengubah label di katalog enum. Seluruh baris lama — termasuk pesanan Budi yang dimasukkan di awal chapter — langsung terbaca dengan label baru tanpa satu pun UPDATE pada tabel pesanan: nilai fisik yang tersimpan tidak berubah, hanya namanya. Urutan pun tetap: enum_range() memperlihatkan menunggu_pembayaran tetap di posisi pertama.

Enum vs text+CHECK vs Tabel Referensi #

Dua alternatif klasik untuk daftar nilai tertutup adalah text dengan CHECK constraint, dan tabel referensi yang divalidasi lewat foreign key. Ketiganya membatasi nilai, tetapi berbeda pada urutan, evolusi, dan biaya penyimpanan:

-- alternatif pertama: text + CHECK constraint
CREATE TABLE pesanan_teks (
    id      integer PRIMARY KEY,
    status  text NOT NULL
        CHECK (status IN ('menunggu_bayar', 'diproses', 'dikirim', 'selesai'))
);

INSERT INTO pesanan_teks (id, status) VALUES
    (1, 'selesai'),
    (2, 'diproses'),
    (3, 'menunggu_bayar');

-- versi text: ORDER BY jatuh ke urutan ALFABETIS, alur kerja hilang
SELECT status, count(*) AS jumlah
FROM pesanan_teks
GROUP BY status
ORDER BY status;

-- versi enum pada tabel yang sama datanya: urutan deklarasi menang
SELECT status, count(*) AS jumlah
FROM pesanan
GROUP BY status
ORDER BY status;

-- alternatif kedua: tabel referensi + FK; menambah nilai = INSERT biasa
CREATE TABLE status_referensi (
    status  text PRIMARY KEY,
    label   text NOT NULL
);

INSERT INTO status_referensi (status, label) VALUES
    ('menunggu_bayar', 'Menunggu Pembayaran'),
    ('diproses',       'Diproses'),
    ('dikirim',        'Dikirim'),
    ('selesai',        'Selesai'),
    ('retur',          'Retur');

SELECT status, label
FROM status_referensi
ORDER BY status;

Output yang Dihasilkan:

text
CREATE TABLE
INSERT 0 3
     status     | jumlah 
----------------+--------
 diproses       |      1
 menunggu_bayar |      1
 selesai        |      1
(3 rows)

       status        | jumlah 
---------------------+--------
 menunggu_pembayaran |      1
 diproses            |      2
 dikirim             |      1
 dibatalkan          |      1
 selesai             |      1
(5 rows)

CREATE TABLE
INSERT 0 5
     status     |        label        
----------------+---------------------
 dikirim        | Dikirim
 diproses       | Diproses
 menunggu_bayar | Menunggu Pembayaran
 retur          | Retur
 selesai        | Selesai
(5 rows)

Perhatikan dua laporan pertama — datanya sama, kolomnya sama-sama berisi status, tetapi versi text terurut alfabetis (diproses, menunggu_bayar, selesai) sementara versi enum terurut alur kerja. Tabel status_referensi memperlihatkan alternatif kedua: menambah nilai retur cukup dengan INSERT biasa, tanpa ALTER TYPE, dan setiap nilai bisa membawa metadata seperti label siap-tampil — opsional pula untuk menghapus atau menonaktifkannya.

Aspekenumtext + CHECKTabel referensi + FK
Validasi nilai tak dikenalDitolak sejak parse/INSERTDitolak oleh CHECKDitolak oleh FK
Urutan bermakna untuk pipelineYa, urutan deklarasiTidak, alfabetisTidak otomatis (butuh kolom urutan)
Penyimpanan4 byte per nilaiPanjang teks + 1-2 bytePanjang teks + JOIN saat baca label
Menambah nilaiALTER TYPE ADD VALUE (DDL, tak bisa dibatalkan per nilai)ALTER TABLE ... DROP/ADD CONSTRAINT (DDL)INSERT biasa (DML)
Menghapus nilaiTidak bisaBisa lewat ganti CHECKBisa lewat DELETE/nonaktifkan
Metadata per nilaiTidak adaTidak adaBebas (label, urutan, aktif)

Enum menang bila daftarnya benar-benar tertutup dan stabil — status pesanan, jenis kelamin, level log — dan urutan deklarasinya sekaligus menjadi urutan bisnis. Begitu nilai perlu sering ditambah/dihapus, membawa metadata, atau dipakai lintas sistem yang tidak mengenal enum Anda, tabel referensi menjadi pilihan yang lebih sehat meski menagih satu JOIN.

Enum membatasi nilai apa saja yang boleh masuk. Tipe kustom berikutnya membatasi bentuk dan rentang nilai pada tipe yang sudah ada: domain — misalnya kode_pos yang wajib lima digit, atau harga yang tidak boleh negatif — dengan sekali definisi yang terpakai di banyak tabel.

Referensi #

Berkas Sumber

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

add-value-aman.sql

Unduh berkas
sql
-- di luar transaksi eksplisit, ADD VALUE langsung ter-commit dan bisa dipakai
ALTER TYPE status_pesanan ADD VALUE IF NOT EXISTS 'dibatalkan' BEFORE 'selesai';

INSERT INTO pesanan (id, pelanggan, status, dibuat) VALUES
    (6, 'Dewi', 'dibatalkan', DATE '2026-01-10');

SELECT id, pelanggan, status
FROM pesanan
ORDER BY status, id;

SELECT enum_range(NULL::status_pesanan) AS daftar_status;

add-value-transaksi.sql

Unduh berkas
sql
BEGIN;
ALTER TYPE status_pesanan ADD VALUE 'dibatalkan';
SELECT 'dibatalkan'::status_pesanan AS tes_nilai_baru;
COMMIT;

buat-enum.sql

Unduh berkas
sql
-- enum: daftar nilai tertutup dengan urutan mengikuti urutan PENDEKLARASIAN
CREATE TYPE status_pesanan AS ENUM (
    'menunggu_bayar',
    'diproses',
    'dikirim',
    'selesai'
);

CREATE TABLE pesanan (
    id         integer PRIMARY KEY,
    pelanggan  text NOT NULL,
    status     status_pesanan NOT NULL DEFAULT 'menunggu_bayar',
    dibuat     date NOT NULL
);

INSERT INTO pesanan (id, pelanggan, status, dibuat) VALUES
    (1, 'Budi', 'selesai',        DATE '2026-01-05'),
    (2, 'Siti', 'diproses',       DATE '2026-01-06'),
    (3, 'Budi', 'menunggu_bayar', DATE '2026-01-07'),
    (4, 'Dewi', 'dikirim',        DATE '2026-01-08'),
    (5, 'Siti', 'diproses',       DATE '2026-01-09');

-- ORDER BY mengikuti urutan deklarasi, bukan urutan alfabetis
SELECT id, pelanggan, status, dibuat
FROM pesanan
ORDER BY status, id;

enum-vs-alternatif.sql

Unduh berkas
sql
-- alternatif pertama: text + CHECK constraint
CREATE TABLE pesanan_teks (
    id      integer PRIMARY KEY,
    status  text NOT NULL
        CHECK (status IN ('menunggu_bayar', 'diproses', 'dikirim', 'selesai'))
);

INSERT INTO pesanan_teks (id, status) VALUES
    (1, 'selesai'),
    (2, 'diproses'),
    (3, 'menunggu_bayar');

-- versi text: ORDER BY jatuh ke urutan ALFABETIS, alur kerja hilang
SELECT status, count(*) AS jumlah
FROM pesanan_teks
GROUP BY status
ORDER BY status;

-- versi enum pada tabel yang sama datanya: urutan deklarasi menang
SELECT status, count(*) AS jumlah
FROM pesanan
GROUP BY status
ORDER BY status;

-- alternatif kedua: tabel referensi + FK; menambah nilai = INSERT biasa
CREATE TABLE status_referensi (
    status  text PRIMARY KEY,
    label   text NOT NULL
);

INSERT INTO status_referensi (status, label) VALUES
    ('menunggu_bayar', 'Menunggu Pembayaran'),
    ('diproses',       'Diproses'),
    ('dikirim',        'Dikirim'),
    ('selesai',        'Selesai'),
    ('retur',          'Retur');

SELECT status, label
FROM status_referensi
ORDER BY status;

rename-value.sql

Unduh berkas
sql
-- RENAME VALUE mengganti label tanpa mengubah urutan (PostgreSQL 10+)
ALTER TYPE status_pesanan RENAME VALUE 'menunggu_bayar' TO 'menunggu_pembayaran';

-- data lama otomatis terbaca dengan label barunya
SELECT id, pelanggan, status
FROM pesanan
WHERE status = 'menunggu_pembayaran'
ORDER BY id;

SELECT enum_range(NULL::status_pesanan) AS daftar_status;

urutan-enum.sql

Unduh berkas
sql
-- perbandingan mengikuti urutan deklarasi: >= 'diproses' berarti diproses ke atas
SELECT id, pelanggan, status
FROM pesanan
WHERE status >= 'diproses'
ORDER BY status, id;

-- rentang nilai: pesanan yang sedang berjalan (diproses s.d. dikirim)
SELECT id, pelanggan, status
FROM pesanan
WHERE status BETWEEN 'diproses' AND 'dikirim'
ORDER BY status, id;

-- enum_range: seluruh nilai yang sah, siap diisi ke dropdown aplikasi
SELECT enum_range(NULL::status_pesanan) AS daftar_status;

-- laporan pipeline: baris per status, terurut mengikuti alur kerja
SELECT status, count(*) AS jumlah
FROM pesanan
GROUP BY status
ORDER BY status;