Chapter sebelumnya memperkenalkan partitioning sebagai pemecahan satu tabel logika menjadi banyak tabel fisik. Strategi pemecahan yang paling banyak dipakai di dunia nyata adalah range partitioning: setiap partisi memegang satu rentang nilai berurutan — bulan ini, kuartal ini, atau nomor seri tertentu. Data penjualan Toko Maju Jaya adalah kandidat sempurna karena hampir semua query-nya menyaring tanggal: laporan bulanan, rekap mingguan, hingga pembandingan bulan-ke-bulan. Chapter ini membahas sintaks PARTITION BY RANGE, aturan batas atas yang eksklusif, perilaku PostgreSQL saat baris jatuh di luar semua rentang, partisi DEFAULT sebagai jaring pengaman, hingga range multi-kolom.
Sintaks Dasar PARTITION BY RANGE #
Range partitioning dimulai dengan mendeklarasikan partition key pada tabel induk, lalu membuat partisi satu per satu dengan batas FROM dan TO. Kita bangun kembali tabel log_penjualan dengan tiga partisi bulanan untuk kuartal pertama 2026:
-- Tabel induk dengan partition key tanggal (tipe date).
CREATE TABLE log_penjualan (
id bigint NOT NULL,
tanggal date NOT NULL,
kota text NOT NULL,
total numeric(12,2) NOT NULL
) PARTITION BY RANGE (tanggal);
-- Tiga partisi bulanan untuk kuartal pertama 2026.
-- Batas bawah ikut masuk, batas atas TIDAK ikut masuk.
CREATE TABLE log_penjualan_2026_01 PARTITION OF log_penjualan
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE log_penjualan_2026_02 PARTITION OF log_penjualan
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
CREATE TABLE log_penjualan_2026_03 PARTITION OF log_penjualan
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
-- INSERT tetap ditujukan ke tabel induk, bukan ke partisi.
INSERT INTO log_penjualan VALUES
(1, DATE '2026-01-05', 'Jakarta', 125000.00),
(2, DATE '2026-01-18', 'Bandung', 64000.00),
(3, DATE '2026-02-02', 'Surabaya', 210000.00),
(4, DATE '2026-02-23', 'Jakarta', 87500.00),
(5, DATE '2026-03-09', 'Medan', 45000.00),
(6, DATE '2026-03-29', 'Bandung', 132000.00);
SELECT id, tanggal, kota, total FROM log_penjualan ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 6
id | tanggal | kota | total
----+------------+----------+-----------
1 | 2026-01-05 | Jakarta | 125000.00
2 | 2026-01-18 | Bandung | 64000.00
3 | 2026-02-02 | Surabaya | 210000.00
4 | 2026-02-23 | Jakarta | 87500.00
5 | 2026-03-09 | Medan | 45000.00
6 | 2026-03-29 | Bandung | 132000.00
(6 rows)Mari kita bedah baris-baris pentingnya:
- Baris
PARTITION BY RANGE (tanggal): tabel induk dideklarasikan terpartisi berdasarkan rentang nilai kolomtanggal; kolom inilah partition key. - Baris
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01'): partisi pertama menampung baris dengantanggal >= '2026-01-01'dantanggal < '2026-02-01'— batas bawah ikut dimasukkan, batas atas tidak. - Baris
INSERT INTO log_penjualan ...: enam baris dimasukkan lewat induk tanpa menyebut nama partisi; query penutup dari induk mengembalikan keenam baris tersebut.
Batas Atas Eksklusif dan Routing Baris #
Aturan “batas atas tidak ikut” membuat batas partisi bertetangga saling menyambung tanpa tumpang tindih dan tanpa celah: TO ('2026-02-01') pada partisi Januari adalah titik yang sama dengan FROM ('2026-02-01') pada partisi Februari. File berikut menguji tepi batas tersebut sambil membuktikan routing:
-- Tiga baris baru; perhatikan tanggal 2026-03-01 (batas bawah Maret).
INSERT INTO log_penjualan VALUES
(7, DATE '2026-01-31', 'Surabaya', 99000.00),
(8, DATE '2026-02-28', 'Medan', 155000.00),
(9, DATE '2026-03-01', 'Jakarta', 47000.00);
-- Membaca langsung dari sebuah partisi hanya memperlihatkan
-- baris yang batasnya memuat partisi tersebut.
SELECT id, tanggal, kota, total FROM log_penjualan_2026_01 ORDER BY id;
SELECT id, tanggal, kota, total FROM log_penjualan_2026_02 ORDER BY id;
SELECT id, tanggal, kota, total FROM log_penjualan_2026_03 ORDER BY id;Output yang Dihasilkan:
INSERT 0 3
id | tanggal | kota | total
----+------------+----------+-----------
1 | 2026-01-05 | Jakarta | 125000.00
2 | 2026-01-18 | Bandung | 64000.00
7 | 2026-01-31 | Surabaya | 99000.00
(3 rows)
id | tanggal | kota | total
----+------------+----------+-----------
3 | 2026-02-02 | Surabaya | 210000.00
4 | 2026-02-23 | Jakarta | 87500.00
8 | 2026-02-28 | Medan | 155000.00
(3 rows)
id | tanggal | kota | total
----+------------+---------+-----------
5 | 2026-03-09 | Medan | 45000.00
6 | 2026-03-29 | Bandung | 132000.00
9 | 2026-03-01 | Jakarta | 47000.00
(3 rows)Baris dengan tanggal = '2026-03-01' (id 9) mendarat di partisi Maret, bukan Februari — tanggal itu adalah batas bawah Maret sekaligus batas atas eksklusif Februari. Hasil pembacaan langsung dari log_penjualan_2026_01 sampai log_penjualan_2026_03 menegaskan bahwa setiap partisi hanya berisi baris bulannya sendiri: tiga baris Januari, tiga baris Februari, tiga baris Maret. Membaca dari partisi secara langsung seperti ini sah dilakukan dan berguna untuk inspeksi, meskipun aplikasi sehari-hari tetap sebaiknya berbicara ke induk.
Contoh Lengkap: Partisi Bulanan Satu Semester #
Pola produksi yang lazim membuat partisi bulanan jauh sebelum datanya tiba, lalu mengisinya lewat induk. File berikut melengkapi April sampai Juni, mengisi 8.100 baris transaksi sintetis untuk satu semester 2026, lalu merekap penyebaran baris per tabel fisik:
-- Lengkapi partisi April-Juni sebelum mengisi data satu semester penuh.
CREATE TABLE log_penjualan_2026_04 PARTITION OF log_penjualan
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
CREATE TABLE log_penjualan_2026_05 PARTITION OF log_penjualan
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
CREATE TABLE log_penjualan_2026_06 PARTITION OF log_penjualan
FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
INSERT INTO log_penjualan
SELECT g,
DATE '2026-01-01' + (g % 181),
(ARRAY['Bandung', 'Jakarta', 'Medan', 'Surabaya'])[1 + (g % 4)],
((g * 37) % 1000 + 1) * 1000.00
FROM generate_series(1000, 9099) AS g;
-- Rekap penyebaran baris per tabel fisik (termasuk DEFAULT).
SELECT tableoid::regclass AS partisi, count(*) AS baris
FROM log_penjualan
GROUP BY 1
ORDER BY 1;Output yang Dihasilkan:
CREATE TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 8100
partisi | baris
-----------------------+-------
log_penjualan_2026_01 | 1398
log_penjualan_2026_02 | 1254
log_penjualan_2026_03 | 1367
log_penjualan_2026_04 | 1345
log_penjualan_2026_05 | 1395
log_penjualan_2026_06 | 1350
(6 rows)Baris INSERT 0 8100 memasukkan data satu semester penuh tanpa satu pun penyebutan nama partisi, dan rekap tableoid menunjukkan 8.109 baris terbagi rapi ke enam partisi bulanan: 1.398 di Januari, 1.254 di Februari, dan seterusnya hingga 1.350 di Juni. Selisih antar-bulan mengikuti jumlah hari kalender — Februari 2026 yang berjumlah 28 hari memang menampung paling sedikit. Inilah bentuk lengkap tabel penjualan yang siap dipakai sepanjang semester.
Baris di Luar Semua Rentang #
Apa yang terjadi jika datang transaksi dari bulan yang partisinya belum dibuat? PostgreSQL menolak baris tersebut dengan error, bukan memasukkannya diam-diam ke partisi terdekat:
-- Juli 2026 belum memiliki partisi.
INSERT INTO log_penjualan VALUES
(10000, DATE '2026-07-05', 'Jakarta', 77000.00);Output yang Dihasilkan:
psql:insert-tanpa-partisi.sql:3: ERROR: no partition of relation "log_penjualan" found for row
DETAIL: Partition key of the failing row contains (tanggal) = (2026-07-05).Pesan errornya sangat spesifik: tidak ada partisi dari relasi log_penjualan untuk baris itu, dan baris bermasalah memuat nilai (tanggal) = (2026-07-05). Kegagalan cepat seperti ini justru baik — kita langsung tahu kalender partisi telat diperbarui, bukan menemukan data hilang berminggu-minggu kemudian. Pola produksi yang mengandalkan kegagalan ini biasa menjalankan pekerjaan terjadwal yang membuat partisi bulan depan sebelum tanggal berganti.
Partisi DEFAULT sebagai Jaring Pengaman #
Jika penolakan keras tidak diinginkan — misalnya data dari sistem lama bisa membawa tanggal di luar perkiraan — partisi DEFAULT menampung semua baris yang tidak cocok dengan partisi mana pun:
-- Partisi DEFAULT menampung baris yang tidak masuk partisi mana pun.
CREATE TABLE log_penjualan_default PARTITION OF log_penjualan DEFAULT;
-- INSERT yang barusan gagal kini berhasil.
INSERT INTO log_penjualan VALUES
(10000, DATE '2026-07-05', 'Jakarta', 77000.00),
(10001, DATE '2026-08-20', 'Medan', 38000.00);
SELECT id, tanggal, kota, total FROM log_penjualan_default ORDER BY id;
SELECT count(*) AS total_via_induk FROM log_penjualan;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 2
id | tanggal | kota | total
-------+------------+---------+----------
10000 | 2026-07-05 | Jakarta | 77000.00
10001 | 2026-08-20 | Medan | 38000.00
(2 rows)
total_via_induk
-----------------
8111
(1 row)Dua INSERT yang barusan gagal kini berhasil: INSERT 0 2 menandai masuknya baris 5 Juli dan 20 Agustus, dan pembacaan langsung dari log_penjualan_default memperlihatkan keduanya. Total via induk naik menjadi 8.111 baris. Partisi DEFAULT hanya boleh ada satu per tabel induk dan sebaiknya dipakai sebagai penampung sementara, bukan tempat pembuangan permanen — baris di dalamnya tetap terbaca oleh query, tetapi keberadaannya mengubah beberapa perilaku pemeliharaan.
Range Multi-Kolom #
Partition key boleh berisi lebih dari satu kolom; pasangan nilainya dibandingkan seperti urutan kamus — kolom pertama dibandingkan lebih dulu, kolom kedua menjadi pemutus saat kolom pertama seri. Contoh berikut mempartisi tabel pengiriman berdasarkan pasangan (kota, tanggal):
-- Range multi-kolom: pasangan nilai dibandingkan seperti urutan kamus
-- (kota dibanding lebih dulu, lalu tanggal).
CREATE TABLE pengiriman (
kota text NOT NULL,
tanggal date NOT NULL,
berat numeric(10,2) NOT NULL
) PARTITION BY RANGE (kota, tanggal);
CREATE TABLE pengiriman_bandung_januari PARTITION OF pengiriman
FOR VALUES FROM ('Bandung', '2026-01-01') TO ('Bandung', '2026-02-01');
CREATE TABLE pengiriman_bandung_lanjutan PARTITION OF pengiriman
FOR VALUES FROM ('Bandung', '2026-02-01') TO ('Jakarta', '2026-01-01');
CREATE TABLE pengiriman_jakarta PARTITION OF pengiriman
FOR VALUES FROM ('Jakarta', '2026-01-01') TO ('Medan', '2026-01-01');
INSERT INTO pengiriman VALUES
('Bandung', DATE '2026-01-15', 3.50),
('Bandung', DATE '2026-03-02', 8.00),
('Jakarta', DATE '2026-02-10', 12.00);
SELECT kota, tanggal, berat, tableoid::regclass AS partisi
FROM pengiriman
ORDER BY kota, tanggal;Output yang Dihasilkan:
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 3
kota | tanggal | berat | partisi
---------+------------+-------+-----------------------------
Bandung | 2026-01-15 | 3.50 | pengiriman_bandung_januari
Bandung | 2026-03-02 | 8.00 | pengiriman_bandung_lanjutan
Jakarta | 2026-02-10 | 12.00 | pengiriman_jakarta
(3 rows)Partisi pengiriman_bandung_januari memuat semua baris dengan kota = 'Bandung' dan tanggal di Januari 2026; partisi pengiriman_bandung_lanjutan menampung Bandung mulai Februari ke atas (batas atasnya adalah pasangan ('Jakarta', '2026-01-01'), artinya kota apa pun yang kurang dari Jakarta); dan pengiriman_jakarta menampung seluruh tanggal untuk kota Jakarta. Hasil routing membuktikannya: kiriman Bandung 15 Januari, kiriman Bandung 2 Maret, dan kiriman Jakarta 10 Februari masing-masing mendarat di partisi yang berbeda sesuai urutan kamus pasangan nilainya.
Rentang tanggal adalah strategi paling natural untuk data deret waktu, tetapi tidak semua data punya urutan alami seperti tanggal. Ketika pemecahannya berdasarkan daftar nilai diskret — kota, kategori produk, atau kode region — strategi yang tepat adalah list 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 tanggal (tipe date).
CREATE TABLE log_penjualan (
id bigint NOT NULL,
tanggal date NOT NULL,
kota text NOT NULL,
total numeric(12,2) NOT NULL
) PARTITION BY RANGE (tanggal);
-- Tiga partisi bulanan untuk kuartal pertama 2026.
-- Batas bawah ikut masuk, batas atas TIDAK ikut masuk.
CREATE TABLE log_penjualan_2026_01 PARTITION OF log_penjualan
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE log_penjualan_2026_02 PARTITION OF log_penjualan
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
CREATE TABLE log_penjualan_2026_03 PARTITION OF log_penjualan
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
-- INSERT tetap ditujukan ke tabel induk, bukan ke partisi.
INSERT INTO log_penjualan VALUES
(1, DATE '2026-01-05', 'Jakarta', 125000.00),
(2, DATE '2026-01-18', 'Bandung', 64000.00),
(3, DATE '2026-02-02', 'Surabaya', 210000.00),
(4, DATE '2026-02-23', 'Jakarta', 87500.00),
(5, DATE '2026-03-09', 'Medan', 45000.00),
(6, DATE '2026-03-29', 'Bandung', 132000.00);
SELECT id, tanggal, kota, total FROM log_penjualan ORDER BY id;
bulanan-lengkap.sql
Unduh berkas-- Lengkapi partisi April-Juni sebelum mengisi data satu semester penuh.
CREATE TABLE log_penjualan_2026_04 PARTITION OF log_penjualan
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
CREATE TABLE log_penjualan_2026_05 PARTITION OF log_penjualan
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
CREATE TABLE log_penjualan_2026_06 PARTITION OF log_penjualan
FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
INSERT INTO log_penjualan
SELECT g,
DATE '2026-01-01' + (g % 181),
(ARRAY['Bandung', 'Jakarta', 'Medan', 'Surabaya'])[1 + (g % 4)],
((g * 37) % 1000 + 1) * 1000.00
FROM generate_series(1000, 9099) AS g;
-- Rekap penyebaran baris per tabel fisik (termasuk DEFAULT).
SELECT tableoid::regclass AS partisi, count(*) AS baris
FROM log_penjualan
GROUP BY 1
ORDER BY 1;
insert-tanpa-partisi.sql
Unduh berkas-- Juli 2026 belum memiliki partisi.
INSERT INTO log_penjualan VALUES
(10000, DATE '2026-07-05', 'Jakarta', 77000.00);
partisi-default.sql
Unduh berkas-- Partisi DEFAULT menampung baris yang tidak masuk partisi mana pun.
CREATE TABLE log_penjualan_default PARTITION OF log_penjualan DEFAULT;
-- INSERT yang barusan gagal kini berhasil.
INSERT INTO log_penjualan VALUES
(10000, DATE '2026-07-05', 'Jakarta', 77000.00),
(10001, DATE '2026-08-20', 'Medan', 38000.00);
SELECT id, tanggal, kota, total FROM log_penjualan_default ORDER BY id;
SELECT count(*) AS total_via_induk FROM log_penjualan;
range-multi-kolom.sql
Unduh berkas-- Range multi-kolom: pasangan nilai dibandingkan seperti urutan kamus
-- (kota dibanding lebih dulu, lalu tanggal).
CREATE TABLE pengiriman (
kota text NOT NULL,
tanggal date NOT NULL,
berat numeric(10,2) NOT NULL
) PARTITION BY RANGE (kota, tanggal);
CREATE TABLE pengiriman_bandung_januari PARTITION OF pengiriman
FOR VALUES FROM ('Bandung', '2026-01-01') TO ('Bandung', '2026-02-01');
CREATE TABLE pengiriman_bandung_lanjutan PARTITION OF pengiriman
FOR VALUES FROM ('Bandung', '2026-02-01') TO ('Jakarta', '2026-01-01');
CREATE TABLE pengiriman_jakarta PARTITION OF pengiriman
FOR VALUES FROM ('Jakarta', '2026-01-01') TO ('Medan', '2026-01-01');
INSERT INTO pengiriman VALUES
('Bandung', DATE '2026-01-15', 3.50),
('Bandung', DATE '2026-03-02', 8.00),
('Jakarta', DATE '2026-02-10', 12.00);
SELECT kota, tanggal, berat, tableoid::regclass AS partisi
FROM pengiriman
ORDER BY kota, tanggal;
routing.sql
Unduh berkas-- Tiga baris baru; perhatikan tanggal 2026-03-01 (batas bawah Maret).
INSERT INTO log_penjualan VALUES
(7, DATE '2026-01-31', 'Surabaya', 99000.00),
(8, DATE '2026-02-28', 'Medan', 155000.00),
(9, DATE '2026-03-01', 'Jakarta', 47000.00);
-- Membaca langsung dari sebuah partisi hanya memperlihatkan
-- baris yang batasnya memuat partisi tersebut.
SELECT id, tanggal, kota, total FROM log_penjualan_2026_01 ORDER BY id;
SELECT id, tanggal, kota, total FROM log_penjualan_2026_02 ORDER BY id;
SELECT id, tanggal, kota, total FROM log_penjualan_2026_03 ORDER BY id;