Tabel terpartisi bukan objek yang selesai begitu dibuat — tabel itu hidup mengikuti kalender. Setiap bulan berganti, partisi bulan baru harus sudah tersedia sebelum transaksi pertamanya tiba; kalau telat, setiap INSERT bulan itu gagal. Di ujung lain, partisi lama yang tak lagi dikueri rutin sebaiknya diputar keluar dari induknya agar tabel kerja tetap ramping. Kabar baiknya: seluruh siklus hidup ini dikerjakan tiga perintah — CREATE TABLE ... PARTITION OF untuk melahirkan, DETACH PARTITION untuk melepas, ATTACH PARTITION untuk memasang kembali — dan semuanya bisa diotomatiskan dengan fungsi PL/pgSQL. Chapter ini merakit siklus lengkapnya pada tabel log_penjualan Toko Maju Jaya, dari partisi bulan depan hingga pemantauan batas partisi lewat katalog sistem.
Menyiapkan Tabel Dua Bulan #
Kita mulai dari keadaan yang meniru pertengahan Februari 2026: partisi Januari dan Februari sudah terisi, sedangkan Maret belum ada:
-- Tabel penjualan terpartisi bulanan, Februari 2026 adalah "bulan berjalan".
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);
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');
INSERT INTO log_penjualan VALUES
(1, DATE '2026-01-05', 'Jakarta', 95000.00),
(2, DATE '2026-01-22', 'Bandung', 42000.00),
(3, DATE '2026-02-03', 'Jakarta', 187000.00),
(4, DATE '2026-02-11', 'Surabaya', 76000.00),
(5, DATE '2026-02-26', 'Medan', 63000.00);
SELECT tableoid::regclass AS partisi, count(*) AS baris,
sum(total) AS total_nilai
FROM log_penjualan
GROUP BY 1
ORDER BY 1;Output yang Dihasilkan:
CREATE TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 5
partisi | baris | total_nilai
-----------------------+-------+-------------
log_penjualan_2026_01 | 2 | 137000.00
log_penjualan_2026_02 | 3 | 326000.00
(2 rows)Rekap pembuka menunjukkan partisi Januari menampung 2 transaksi senilai Rp137.000 dan partisi Februari 3 transaksi senilai Rp326.000. Tanggal tertinggi di dalam tabel adalah 26 Februari — artinya dalam beberapa hari ke depan transaksi pertama bulan Maret akan tiba, dan kalender partisi harus disiapkan sebelum itu terjadi.
Membuat Partisi Bulan Depan #
Pola produksi yang sehat membuat partisi sebelum dibutuhkan, bukan saat error pertama muncul. Membuat partisi di muka tidak mengganggu apa pun: tabel kosong baru dibuat, routing baris yang sudah berjalan tidak berubah, dan index terpartisi induk otomatis menjalar ke partisi baru:
-- Buat partisi Maret SEBELUM bulan berganti.
CREATE TABLE log_penjualan_2026_03 PARTITION OF log_penjualan
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
-- Transaksi pertama Maret langsung punya tempat.
INSERT INTO log_penjualan VALUES
(6, DATE '2026-03-02', 'Jakarta', 110000.00);
SELECT id, tanggal, kota, total FROM log_penjualan ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 1
id | tanggal | kota | total
----+------------+----------+-----------
1 | 2026-01-05 | Jakarta | 95000.00
2 | 2026-01-22 | Bandung | 42000.00
3 | 2026-02-03 | Jakarta | 187000.00
4 | 2026-02-11 | Surabaya | 76000.00
5 | 2026-02-26 | Medan | 63000.00
6 | 2026-03-02 | Jakarta | 110000.00
(6 rows)Partisi log_penjualan_2026_03 dibuat ketika belum ada satu baris Maret pun, lalu transaksi 2 Maret masuk mulus sebagai baris keenam. Jarak antara pembuatan partisi dan kebutuhannya bisa selebar mungkin — sebagian tim membuat partisi 3-6 bulan ke depan sekaligus supaya jadwal pemeliharaan lebih longgar. Sebaliknya, menunda pembuatan partisi sampai error no partition of relation ... found for row muncul berarti transaksi pelanggan ikut gagal — kegagalan yang sepenuhnya bisa dicegah.
Rotasi Data Lama dengan DETACH #
Di ujung umur yang lain, partisi Januari sudah jarang dikueri dan saatnya diarsipkan. DETACH PARTITION melepas partisi dari induk dan menjadikannya tabel biasa — barisnya tidak disalin, tidak dihapus, cukup diputus dari katalog:
-- Rotasi data lama: lepas Januari dari induk (O(1), tanpa menyalin baris).
ALTER TABLE log_penjualan DETACH PARTITION log_penjualan_2026_01;
-- Induk kini hanya memuat Februari dan Maret.
SELECT count(*) AS baris_via_induk FROM log_penjualan;
-- Tabel Januari berdiri sendiri dan tetap bisa dikueri langsung.
SELECT id, tanggal, kota, total FROM log_penjualan_2026_01 ORDER BY id;
SELECT pg_size_pretty(pg_relation_size('log_penjualan_2026_01')) AS ukuran_tabel_arsip;Output yang Dihasilkan:
ALTER TABLE
baris_via_induk
-----------------
4
(1 row)
id | tanggal | kota | total
----+------------+---------+----------
1 | 2026-01-05 | Jakarta | 95000.00
2 | 2026-01-22 | Bandung | 42000.00
(2 rows)
ukuran_tabel_arsip
--------------------
8192 bytes
(1 row)Setelah DETACH, induk hanya memuat 4 baris (Februari dan Maret), sedangkan log_penjualan_2026_01 berdiri sebagai tabel mandiri berisi 2 baris senilai 8192 byte yang tetap bisa dikueri langsung. Dari sini jalur arsip terbuka lebar: COPY log_penjualan_2026_01 TO 'arsip-2026-01.csv' untuk mengekspor, memindahkan tabelnya ke tablespace penyimpanan murah, atau mengompresnya sebagai dump. Bandingkan dengan tabel monolitik yang harus menjalankan DELETE jutaan baris lalu VACUUM — DETACH menyelesaikannya dalam hitungan milidetik tanpa satu baris pun disentuh.
Memasang Kembali dengan ATTACH #
Data arsip bukan barang permanen terkunci: analisis tahunan kerap butuh data lama kembali online. ATTACH PARTITION memasang tabel mandiri itu kembali menjadi partisi dengan batas yang kita sebutkan, dan index yang setara menempel otomatis pada index terpartisi induk (perilaku yang dibuktikan di chapter Partitioned Index):
-- Analisis ulang butuh data Januari? Pasang kembali partisinya.
ALTER TABLE log_penjualan ATTACH PARTITION log_penjualan_2026_01
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
SELECT count(*) AS total_setelah_attach FROM log_penjualan;Output yang Dihasilkan:
ALTER TABLE
total_setelah_attach
----------------------
6
(1 row)Total via induk kembali menjadi 6 baris — dua baris Januari bergabung tanpa penyalinan data, dan pruning kini mengenali partisi itu sebagai bagian dari tabel. Dua syarat harus dipenuhi saat ATTACH: batas yang disebutkan tidak boleh bertabrakan dengan partisi lain, dan seluruh baris tabel harus benar-benar jatuh di dalam batas itu — bila ada baris yang melanggar, ATTACH ditolak dengan error constraint.
Otomatisasi dengan Fungsi PL/pgSQL #
Melakukan semua ini manual tiap bulan adalah ajakan lupa. Pola produksi mengemasnya sebagai fungsi yang dipanggil terjadwal — lewat cron di server, pg_cron di dalam database, atau scheduler aplikasi. Fungsinya sendiri ringkas:
-- Fungsi PL/pgSQL: pastikan partisi bulan tertentu sudah ada.
CREATE OR REPLACE FUNCTION buat_partisi_bulan(target date)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
nama_partisi text;
awal_bulan date;
BEGIN
awal_bulan := date_trunc('month', target)::date;
nama_partisi := 'log_penjualan_' || to_char(awal_bulan, 'YYYY_MM');
IF to_regclass('public.' || nama_partisi) IS NOT NULL THEN
RETURN nama_partisi || ' sudah ada';
END IF;
EXECUTE format(
'CREATE TABLE %I PARTITION OF log_penjualan
FOR VALUES FROM (%L) TO (%L)',
nama_partisi,
awal_bulan,
(awal_bulan + interval '1 month')::date
);
RETURN nama_partisi || ' dibuat';
END;
$$;
-- Panggil untuk Mei: partisi baru dibuat.
SELECT buat_partisi_bulan(DATE '2026-05-10') AS hasil;
-- Panggil lagi: idempoten, tidak error.
SELECT buat_partisi_bulan(DATE '2026-05-10') AS hasil;
-- Partisi buatan fungsi siap menerima data.
INSERT INTO log_penjualan VALUES
(7, DATE '2026-05-02', 'Bandung', 54000.00);
SELECT id, tanggal, kota, total
FROM log_penjualan_2026_05
ORDER BY id;Output yang Dihasilkan:
CREATE FUNCTION
hasil
------------------------------
log_penjualan_2026_05 dibuat
(1 row)
hasil
---------------------------------
log_penjualan_2026_05 sudah ada
(1 row)
INSERT 0 1
id | tanggal | kota | total
----+------------+---------+----------
7 | 2026-05-02 | Bandung | 54000.00
(1 row)Fungsi buat_partisi_bulan menerima tanggal apa pun di bulan target, menghitung batas awal bulannya dengan date_trunc, merangkai nama partisi berpola log_penjualan_YYYY_MM, lalu mengeksekusi CREATE TABLE dinamis lewat format dengan placeholder %I (identifier) dan %L (literal). Dua pemanggilan menunjukkan sifat idempotennya: panggilan pertama untuk Mei mengembalikan dibuat, panggilan kedua mengembalikan sudah ada alih-alih error — pemeriksaan to_regclass menjaga fungsi aman dijalankan berulang. INSERT penutup membuktikan partisi buatan fungsi langsung menerima data. Sintaks PL/pgSQL yang dipakai di sini dibahas dari dasar di section Stored Programs
.
Memantau Batas Partisi lewat Katalog #
Pemeliharaan butuh pemandangan menyeluruh: partisi apa saja yang terpasang, batasnya mulai kapan sampai kapan. Dua katalog menyimpan jawabannya — pg_inherits untuk relasi induk-anak dan pg_partitioned_table untuk strategi partisi induk:
-- Pantau batas setiap partisi lewat katalog sistem.
SELECT c.relname AS partisi,
pg_get_expr(c.relpartbound, c.oid) AS batas
FROM pg_inherits i
JOIN pg_class c ON c.oid = i.inhrelid
WHERE i.inhparent = 'log_penjualan'::regclass
ORDER BY c.relname;
-- Strategi partisi induk tersimpan di pg_partitioned_table.
SELECT pt.partstrat AS strategi
FROM pg_partitioned_table pt
WHERE pt.partrelid = 'log_penjualan'::regclass;Output yang Dihasilkan:
partisi | batas
-----------------------+--------------------------------------------------
log_penjualan_2026_01 | FOR VALUES FROM ('2026-01-01') TO ('2026-02-01')
log_penjualan_2026_02 | FOR VALUES FROM ('2026-02-01') TO ('2026-03-01')
log_penjualan_2026_03 | FOR VALUES FROM ('2026-03-01') TO ('2026-04-01')
log_penjualan_2026_05 | FOR VALUES FROM ('2026-05-01') TO ('2026-06-01')
(4 rows)
strategi
----------
r
(1 row)Kolom batas dihasilkan fungsi pg_get_expr yang membaca atribut relpartbound tiap partisi dan mengembalikan teks FOR VALUES FROM ... TO ... yang persis seperti saat partisi dibuat. Hasilnya memperlihatkan empat partisi terpasang — termasuk log_penjualan_2026_05 buatan fungsi — dengan jendela April yang masih kosong menunggu gilirannya. Strategi induk tercatat sebagai r (range) di pg_partitioned_table. Di sesi psql interaktif, perintah \d+ log_penjualan menampilkan daftar partisi beserta batasnya dalam satu layar — praktis untuk pemeriksaan cepat, sedangkan query katalog di atas lebih cocok untuk skrip monitoring.
Siklus hidup partisi kini lengkap: lahir tepat waktu, bekerja bertahun-tahun, lalu pergi tanpa drama. Ada satu bab sejarah yang belum kita buka — sebelum declarative partitioning hadir di PostgreSQL 10, pemecahan tabel dikerjakan dengan mekanisme inheritance yang sama sekali berbeda karakternya. Memahaminya menjelaskan banyak keputusan desain hari ini, dan itulah topik chapter berikutnya.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
attach-kembali.sql
Unduh berkas-- Analisis ulang butuh data Januari? Pasang kembali partisinya.
ALTER TABLE log_penjualan ATTACH PARTITION log_penjualan_2026_01
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
SELECT count(*) AS total_setelah_attach FROM log_penjualan;
buat-tabel.sql
Unduh berkas-- Tabel penjualan terpartisi bulanan, Februari 2026 adalah "bulan berjalan".
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);
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');
INSERT INTO log_penjualan VALUES
(1, DATE '2026-01-05', 'Jakarta', 95000.00),
(2, DATE '2026-01-22', 'Bandung', 42000.00),
(3, DATE '2026-02-03', 'Jakarta', 187000.00),
(4, DATE '2026-02-11', 'Surabaya', 76000.00),
(5, DATE '2026-02-26', 'Medan', 63000.00);
SELECT tableoid::regclass AS partisi, count(*) AS baris,
sum(total) AS total_nilai
FROM log_penjualan
GROUP BY 1
ORDER BY 1;
detach-arsip.sql
Unduh berkas-- Rotasi data lama: lepas Januari dari induk (O(1), tanpa menyalin baris).
ALTER TABLE log_penjualan DETACH PARTITION log_penjualan_2026_01;
-- Induk kini hanya memuat Februari dan Maret.
SELECT count(*) AS baris_via_induk FROM log_penjualan;
-- Tabel Januari berdiri sendiri dan tetap bisa dikueri langsung.
SELECT id, tanggal, kota, total FROM log_penjualan_2026_01 ORDER BY id;
SELECT pg_size_pretty(pg_relation_size('log_penjualan_2026_01')) AS ukuran_tabel_arsip;
fungsi-bulan.sql
Unduh berkas-- Fungsi PL/pgSQL: pastikan partisi bulan tertentu sudah ada.
CREATE OR REPLACE FUNCTION buat_partisi_bulan(target date)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
nama_partisi text;
awal_bulan date;
BEGIN
awal_bulan := date_trunc('month', target)::date;
nama_partisi := 'log_penjualan_' || to_char(awal_bulan, 'YYYY_MM');
IF to_regclass('public.' || nama_partisi) IS NOT NULL THEN
RETURN nama_partisi || ' sudah ada';
END IF;
EXECUTE format(
'CREATE TABLE %I PARTITION OF log_penjualan
FOR VALUES FROM (%L) TO (%L)',
nama_partisi,
awal_bulan,
(awal_bulan + interval '1 month')::date
);
RETURN nama_partisi || ' dibuat';
END;
$$;
-- Panggil untuk Mei: partisi baru dibuat.
SELECT buat_partisi_bulan(DATE '2026-05-10') AS hasil;
-- Panggil lagi: idempoten, tidak error.
SELECT buat_partisi_bulan(DATE '2026-05-10') AS hasil;
-- Partisi buatan fungsi siap menerima data.
INSERT INTO log_penjualan VALUES
(7, DATE '2026-05-02', 'Bandung', 54000.00);
SELECT id, tanggal, kota, total
FROM log_penjualan_2026_05
ORDER BY id;
pantau-batas.sql
Unduh berkas-- Pantau batas setiap partisi lewat katalog sistem.
SELECT c.relname AS partisi,
pg_get_expr(c.relpartbound, c.oid) AS batas
FROM pg_inherits i
JOIN pg_class c ON c.oid = i.inhrelid
WHERE i.inhparent = 'log_penjualan'::regclass
ORDER BY c.relname;
-- Strategi partisi induk tersimpan di pg_partitioned_table.
SELECT pt.partstrat AS strategi
FROM pg_partitioned_table pt
WHERE pt.partrelid = 'log_penjualan'::regclass;
partisi-bulan-depan.sql
Unduh berkas-- Buat partisi Maret SEBELUM bulan berganti.
CREATE TABLE log_penjualan_2026_03 PARTITION OF log_penjualan
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
-- Transaksi pertama Maret langsung punya tempat.
INSERT INTO log_penjualan VALUES
(6, DATE '2026-03-02', 'Jakarta', 110000.00);
SELECT id, tanggal, kota, total FROM log_penjualan ORDER BY id;