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:
-- 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:
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:
- Baris
PARTITION BY LIST (kota): partition key adalah kolomkota, dan strategi pembagiannya adalah keanggotaan dalam daftar, bukan rentang. - Baris
FOR VALUES IN ('Jakarta', 'Bandung', 'Surabaya'): partisipesanan_jawahanya menerima baris yang kolomkota-nya persis salah satu dari tiga nilai tersebut — nilai lain ditolak. - 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:
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:
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:
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:
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:
psql:multi-kolom-gagal.sql:6: ERROR: cannot use "list" partition strategy with more than one columnPostgreSQL 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:
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 pemakaian | Contoh nilai kunci | Keunggulan dibanding range |
|---|---|---|
| pemisahan region/wilayah | Jakarta, Medan, Denpasar | daftar eksplisit per region, mudah diaudit |
| kategori produk | elektronik, fashion, bahan makanan | pembaruan per kategori tanpa menyentuh kategori lain |
| multi-tenant per pelanggan | kode tenant toko001, toko002 | isolasi data tenant, mudah dipindah saat offboarding |
| kanal penjualan | toko, online, mitra | laporan per kanal menyentuh satu partisi |
| status pemrosesan | baru, diproses, selesai | partisi 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-- 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-- 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-- 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-- 'Denpasar' tidak terdaftar di partisi mana pun.
INSERT INTO pesanan VALUES
(9, 'Denpasar', 'Siti', 150000.00);
partisi-default.sql
Unduh berkas-- 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 berkasINSERT 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;