Range partitioning pada chapter sebelumnya cocok untuk nilai yang punya urutan alami seperti tanggal. Namun data Toko Maju Jaya juga punya atribut yang tidak terurut sama sekali: kota asal pesanan. Tidak ada artinya mempartisi Jakarta sampai Medan sebagai rentang, karena nilai-nilai itu adalah label diskret, bukan angka yang berkesinambungan. Untuk kasus seperti inilah PostgreSQL menyediakan list partitioning: setiap partisi didefinisikan oleh daftar nilai eksplisit — sekumpulan kota, kategori produk, atau kode region — dan baris masuk ke partisi yang memuat nilai partition key-nya. Chapter ini membahas sintaksnya, routing per region, penanganan nilai tak terdaftar, partisi DEFAULT, serta batasan penting yang kita uji langsung: list multi-kolom tidak didukung PostgreSQL 17.

Sintaks PARTITION BY LIST #

Tabel induk dideklarasikan dengan PARTITION BY LIST (kolom), lalu setiap partisi menyebut daftar nilainya lewat FOR VALUES IN (...). Kita bangun tabel pesanan yang dipecah menjadi dua partisi regional:

sql buat-tabel.sql
-- Tabel induk dengan partition key kota (tipe text).
CREATE TABLE pesanan (
    id        bigint NOT NULL,
    kota      text   NOT NULL,
    pelanggan text   NOT NULL,
    total     numeric(12,2) NOT NULL
) PARTITION BY LIST (kota);

-- Dua partisi regional: setiap partisi memegang daftar kota eksplisit.
CREATE TABLE pesanan_jawa PARTITION OF pesanan
    FOR VALUES IN ('Jakarta', 'Bandung', 'Surabaya');
CREATE TABLE pesanan_sumatera PARTITION OF pesanan
    FOR VALUES IN ('Medan', 'Palembang');

-- INSERT tetap ditujukan ke tabel induk.
INSERT INTO pesanan VALUES
    (1, 'Jakarta',   'Budi', 250000.00),
    (2, 'Bandung',   'Siti', 120000.00),
    (3, 'Medan',     'Budi',  75000.00),
    (4, 'Surabaya',  'Ani',  310000.00),
    (5, 'Palembang', 'Siti',  95000.00),
    (6, 'Jakarta',   'Ani',  180000.00);

SELECT id, kota, pelanggan, total FROM pesanan ORDER BY id;

Output yang Dihasilkan:

text
CREATE TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 6
 id |   kota    | pelanggan |   total   
----+-----------+-----------+-----------
  1 | Jakarta   | Budi      | 250000.00
  2 | Bandung   | Siti      | 120000.00
  3 | Medan     | Budi      |  75000.00
  4 | Surabaya  | Ani       | 310000.00
  5 | Palembang | Siti      |  95000.00
  6 | Jakarta   | Ani       | 180000.00
(6 rows)

Mari kita bedah elemen kuncinya:

  1. Baris PARTITION BY LIST (kota): partition key adalah kolom kota, dan strategi pembagiannya adalah keanggotaan dalam daftar, bukan rentang.
  2. Baris FOR VALUES IN ('Jakarta', 'Bandung', 'Surabaya'): partisi pesanan_jawa hanya menerima baris yang kolom kota-nya persis salah satu dari tiga nilai tersebut — nilai lain ditolak.
  3. Baris INSERT INTO pesanan ...: enam pesanan pelanggan Budi, Siti, dan Ani dimasukkan lewat induk seperti biasa; query penutup mengembalikan keenamnya.

Satu nilai hanya boleh muncul di satu partisi; PostgreSQL menolak pembuatan partisi kedua yang memuat Jakarta dengan error overlap. Sebaliknya, daftar setiap partisi boleh memuat nilai berapa pun, dan dua partisi tidak harus berukuran daftar yang sama.

Routing Berdasarkan Daftar Nilai #

Mekanisme routing mengevaluasi nilai kota terhadap daftar tiap partisi, lalu memasukkan baris ke satu-satunya partisi yang cocok. File berikut menambah dua pesanan lalu memeriksa ke mana seluruh baris benar-benar pergi:

sql routing.sql
INSERT INTO pesanan VALUES
    (7, 'Bandung', 'Budi', 64000.00),
    (8, 'Medan',   'Ani', 42000.00);

-- Rekap jumlah baris per partisi.
SELECT urut, tabel, baris FROM (
    SELECT 1 AS urut, 'pesanan_jawa' AS tabel, count(*) AS baris
    FROM pesanan_jawa
    UNION ALL
    SELECT 2, 'pesanan_sumatera', count(*) FROM pesanan_sumatera
) AS rekap ORDER BY urut;

-- lokasi fisik tiap baris menegaskan routing berdasarkan daftar kota.
SELECT id, kota, tableoid::regclass AS lokasi_fisik
FROM pesanan
ORDER BY id;

Output yang Dihasilkan:

text
INSERT 0 2
 urut |      tabel       | baris 
------+------------------+-------
    1 | pesanan_jawa     |     5
    2 | pesanan_sumatera |     3
(2 rows)

 id |   kota    |   lokasi_fisik   
----+-----------+------------------
  1 | Jakarta   | pesanan_jawa
  2 | Bandung   | pesanan_jawa
  3 | Medan     | pesanan_sumatera
  4 | Surabaya  | pesanan_jawa
  5 | Palembang | pesanan_sumatera
  6 | Jakarta   | pesanan_jawa
  7 | Bandung   | pesanan_jawa
  8 | Medan     | pesanan_sumatera
(8 rows)

Rekap jumlah baris menunjukkan partisi pesanan_jawa menampung 5 baris dan pesanan_sumatera 3 baris, sesuai sebaran kota. Kolom lokasi_fisik dari tableoid menegaskan satu per satu: pesanan Jakarta, Bandung, dan Surabaya semuanya tersimpan di pesanan_jawa, sedangkan pesanan Medan dan Palembang di pesanan_sumatera. Perhatikan bahwa routing list tidak peduli urutan nilai — berbeda dari range, tidak ada batas bawah atau atas yang perlu disusun rapi.

Nilai di Luar Daftar #

Ketika tiba pesanan dari kota yang tidak terdaftar di partisi mana pun, PostgreSQL menolaknya dengan error yang menyebut nilai persisnya:

-- 'Denpasar' tidak terdaftar di partisi mana pun.
INSERT INTO pesanan VALUES
    (9, 'Denpasar', 'Siti', 150000.00);

Output yang Dihasilkan:

text
psql:nilai-tak-terdaftar.sql:3: ERROR:  no partition of relation "pesanan" found for row
DETAIL:  Partition key of the failing row contains (kota) = (Denpasar).

Pesan errornya menunjukkan tidak ada partisi dari relasi pesanan untuk baris tersebut, dengan partition key (kota) = (Denpasar). Perilaku ini identik dengan range partitioning: kegagalan cepat dan informatif. Dalam skema regional, error seperti ini biasanya berarti ada kota baru yang perlu diputuskan regionnya — menambahkan 'Denpasar' ke daftar salah satu partisi (atau membuat partisi Bali) akan menyelesaikannya.

Partisi DEFAULT #

Daripada membiarkan kota baru menggagalkan transaksi pelanggan, tabel dapat diberi partisi DEFAULT yang menampung semua nilai tak terdaftar:

-- Partisi DEFAULT menampung kota yang tidak terdaftar.
CREATE TABLE pesanan_lainnya PARTITION OF pesanan DEFAULT;

-- INSERT yang barusan gagal kini berhasil.
INSERT INTO pesanan VALUES
    (9,  'Denpasar', 'Siti', 150000.00),
    (10, 'Makassar', 'Budi',  88000.00);

SELECT id, kota, pelanggan, total FROM pesanan_lainnya ORDER BY id;
SELECT tableoid::regclass AS partisi, count(*) AS baris
FROM pesanan
GROUP BY 1
ORDER BY 1;

Output yang Dihasilkan:

text
CREATE TABLE
INSERT 0 2
 id |   kota   | pelanggan |   total   
----+----------+-----------+-----------
  9 | Denpasar | Siti      | 150000.00
 10 | Makassar | Budi      |  88000.00
(2 rows)

     partisi      | baris 
------------------+-------
 pesanan_jawa     |     5
 pesanan_sumatera |     3
 pesanan_lainnya  |     2
(3 rows)

Dua INSERT yang sebelumnya gagal kini berhasil: pesanan Denpasar dan Makassar mendarat di pesanan_lainnya. Rekap akhir menunjukkan tiga tabel fisik dengan 5, 3, dan 2 baris. Sebaiknya DEFAULT diperlakukan sebagai ruang tunggu: pindahkan isinya ke partisi resmi begitu keputusan pemetaan kota diambil, karena baris yang menumpuk di DEFAULT akan menghambat pembuatan partisi baru yang daftar nilainya bertabrakan.

Batasan List Multi-Kolom #

Range partitioning mendukung beberapa kolom partition key, dan wajar kalau kita mengira list juga begitu. Kenyataannya tidak: kita uji langsung percobaan mempartisi berdasarkan pasangan (gudang, produk):

-- List multi-kolom tidak didukung oleh PostgreSQL 17.
CREATE TABLE stok_gudang (
    gudang text NOT NULL,
    produk text NOT NULL,
    jumlah integer NOT NULL
) PARTITION BY LIST (gudang, produk);

Output yang Dihasilkan:

text
psql:multi-kolom-gagal.sql:6: ERROR:  cannot use "list" partition strategy with more than one column

PostgreSQL 17 menolaknya dengan error cannot use "list" partition strategy with more than one column — strategi list memang hanya menerima satu kolom kunci. Kebutuhan serupa tetap bisa dipenuhi dengan memakai satu expression yang menggabungkan beberapa kolom menjadi satu nilai kunci:

-- Alternatif satu kunci expression untuk dua atribut sekaligus.
CREATE TABLE stok_gudang (
    gudang text NOT NULL,
    produk text NOT NULL,
    jumlah integer NOT NULL
) PARTITION BY LIST ((gudang || '-' || produk));

CREATE TABLE stok_gudang_jakarta PARTITION OF stok_gudang
    FOR VALUES IN ('Jakarta-Kopi', 'Jakarta-Teh');

INSERT INTO stok_gudang VALUES
    ('Jakarta', 'Kopi', 120),
    ('Jakarta', 'Teh',   80);

SELECT gudang, produk, jumlah, tableoid::regclass AS partisi
FROM stok_gudang
ORDER BY produk;

Output yang Dihasilkan:

text
CREATE TABLE
CREATE TABLE
INSERT 0 2
 gudang  | produk | jumlah |       partisi       
---------+--------+--------+---------------------
 Jakarta | Kopi   |    120 | stok_gudang_jakarta
 Jakarta | Teh    |     80 | stok_gudang_jakarta
(2 rows)

Partition key di sini bukan kolom, melainkan expression (gudang || '-' || produk), sehingga baris ('Jakarta', 'Kopi', 120) dievaluasi terhadap daftar 'Jakarta-Kopi'. Kedua baris uji mendarat di stok_gudang_jakarta seperti diharapkan. Konsekuensinya perlu dipahami: nilai kunci gabungan harus konsisten — satu kota ditulis Jakarta di baris A dan JAKARTA di baris B akan menghasilkan kunci berbeda. Expression pembungkus seperti upper(gudang) || '-' || upper(produk) membuat kunci kebal terhadap kapitalisasi.

Kapan Memilih List #

List partitioning bersinar ketika pemisahan data mengikuti kategori yang jumlahnya terbatas dan stabil. Panduan kasus pemakaian:

Kasus pemakaianContoh nilai kunciKeunggulan dibanding range
pemisahan region/wilayahJakarta, Medan, Denpasardaftar eksplisit per region, mudah diaudit
kategori produkelektronik, fashion, bahan makananpembaruan per kategori tanpa menyentuh kategori lain
multi-tenant per pelanggankode tenant toko001, toko002isolasi data tenant, mudah dipindah saat offboarding
kanal penjualantoko, online, mitralaporan per kanal menyentuh satu partisi
status pemrosesanbaru, diproses, selesaipartisi kecil yang aktif terpisah dari arsip

Ketimbang range, list menuntut kita menebak seluruh himpunan nilai di muka; nilai baru yang tidak dipetakan akan jatuh ke DEFAULT atau ditolak. Untuk data yang nilainya tumbuh tanpa batas dan tidak beraturan — atau yang ingin disebar merata demi beban tulis — strategi ketiga hadir: hash partitioning, topik chapter berikutnya.

Referensi #

Berkas Sumber

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

buat-tabel.sql

Unduh berkas
sql
-- Tabel induk dengan partition key kota (tipe text).
CREATE TABLE pesanan (
    id        bigint NOT NULL,
    kota      text   NOT NULL,
    pelanggan text   NOT NULL,
    total     numeric(12,2) NOT NULL
) PARTITION BY LIST (kota);

-- Dua partisi regional: setiap partisi memegang daftar kota eksplisit.
CREATE TABLE pesanan_jawa PARTITION OF pesanan
    FOR VALUES IN ('Jakarta', 'Bandung', 'Surabaya');
CREATE TABLE pesanan_sumatera PARTITION OF pesanan
    FOR VALUES IN ('Medan', 'Palembang');

-- INSERT tetap ditujukan ke tabel induk.
INSERT INTO pesanan VALUES
    (1, 'Jakarta',   'Budi', 250000.00),
    (2, 'Bandung',   'Siti', 120000.00),
    (3, 'Medan',     'Budi',  75000.00),
    (4, 'Surabaya',  'Ani',  310000.00),
    (5, 'Palembang', 'Siti',  95000.00),
    (6, 'Jakarta',   'Ani',  180000.00);

SELECT id, kota, pelanggan, total FROM pesanan ORDER BY id;

ekspresi-kunci.sql

Unduh berkas
sql
-- Alternatif satu kunci expression untuk dua atribut sekaligus.
CREATE TABLE stok_gudang (
    gudang text NOT NULL,
    produk text NOT NULL,
    jumlah integer NOT NULL
) PARTITION BY LIST ((gudang || '-' || produk));

CREATE TABLE stok_gudang_jakarta PARTITION OF stok_gudang
    FOR VALUES IN ('Jakarta-Kopi', 'Jakarta-Teh');

INSERT INTO stok_gudang VALUES
    ('Jakarta', 'Kopi', 120),
    ('Jakarta', 'Teh',   80);

SELECT gudang, produk, jumlah, tableoid::regclass AS partisi
FROM stok_gudang
ORDER BY produk;

multi-kolom-gagal.sql

Unduh berkas
sql
-- List multi-kolom tidak didukung oleh PostgreSQL 17.
CREATE TABLE stok_gudang (
    gudang text NOT NULL,
    produk text NOT NULL,
    jumlah integer NOT NULL
) PARTITION BY LIST (gudang, produk);

nilai-tak-terdaftar.sql

Unduh berkas
sql
-- 'Denpasar' tidak terdaftar di partisi mana pun.
INSERT INTO pesanan VALUES
    (9, 'Denpasar', 'Siti', 150000.00);

partisi-default.sql

Unduh berkas
sql
-- Partisi DEFAULT menampung kota yang tidak terdaftar.
CREATE TABLE pesanan_lainnya PARTITION OF pesanan DEFAULT;

-- INSERT yang barusan gagal kini berhasil.
INSERT INTO pesanan VALUES
    (9,  'Denpasar', 'Siti', 150000.00),
    (10, 'Makassar', 'Budi',  88000.00);

SELECT id, kota, pelanggan, total FROM pesanan_lainnya ORDER BY id;
SELECT tableoid::regclass AS partisi, count(*) AS baris
FROM pesanan
GROUP BY 1
ORDER BY 1;

routing.sql

Unduh berkas
sql
INSERT INTO pesanan VALUES
    (7, 'Bandung', 'Budi', 64000.00),
    (8, 'Medan',   'Ani', 42000.00);

-- Rekap jumlah baris per partisi.
SELECT urut, tabel, baris FROM (
    SELECT 1 AS urut, 'pesanan_jawa' AS tabel, count(*) AS baris
    FROM pesanan_jawa
    UNION ALL
    SELECT 2, 'pesanan_sumatera', count(*) FROM pesanan_sumatera
) AS rekap ORDER BY urut;

-- lokasi fisik tiap baris menegaskan routing berdasarkan daftar kota.
SELECT id, kota, tableoid::regclass AS lokasi_fisik
FROM pesanan
ORDER BY id;