Episode ini membahas table partitioning untuk tabel raksasa: kapan partitioning dibutuhkan, declarative partitioning dengan Range List dan Hash, serta partition pruning yang memotong eksekusi query hanya ke partisi yang relevan untuk data time-series berskala besar.

Selamat datang di episode 18 series Belajar SQL PostgreSQL! Bayangkan tabel events yang mencatat semua log aplikasi — setiap hari bertambah ratusan ribu baris, dan dalam setahun mencapai ratusan juta. Index sudah dibuat, tapi segalanya mulai terasa lambat: backup makin lama, maintenance makin berat, dan query punya proporsi data lama yang semakin menumpuk. Inilah saatnya table partitioning.
Partitioning memecah satu tabel logika besar menjadi beberapa partisi fisik yang lebih kecil, disatukan oleh tabel parent. Secara logika, aplikasi tetap SELECT dari satu tabel — tapi secara fisik, PostgreSQL bisa membuang partisi yang tidak relevan dan menangani data lama dengan lebih efisien. Ini salah satu teknik yang wajib dikuasai untuk membangun database yang bertahan di skala besar.
Di episode ini, kita akan membahas kapan partitioning benar-benar dibutuhkan, tiga strategi declarative partitioning (Range, List, Hash), dan partition pruning yang membuat query hanya menyentuh partisi yang relevan.
Partitioning bukan solusi untuk semua tabel — tabel kecil justru akan lebih merepotkan jika dipartisi. Kapan partitioning bermanfaat?
VACUUM dan index.DROP PARTITION jauh lebih cepat dari DELETE ... WHERE miliaran baris).1. Tabel berisi ratusan juta baris
2. Backup dan VACUUM semakin lambat
3. Query time-series sering menyaring rentang tanggal
4. Data lama perlu diarsip / dihapus rutinTip
Tanda paling objektif bahwa partitioning layak: kalian menghapus atau mengarsip data lama secara berkala, dan delete-nya sendiri sudah terlalu berat. Dengan partitioning range, drop satu partisi bulan lalu hanya butuh menghapus satu file data — hampir instan, tanpa memindai jutaan baris.
PostgreSQL mendukung declarative partitioning: tabel parent dideklarasikan dengan PARTITION BY, lalu partisi-partisi dibuat secara eksplisit. Tidak ada trigger yang perlu dipelihara — routing otomatis ditangani engine.
Strategi paling umum untuk time-series: partisi berdasarkan rentang tanggal. Setiap partisi menangani satu periode (hari, minggu, atau bulan).
CREATE TABLE events (
id BIGINT GENERATED ALWAYS AS IDENTITY,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL
) PARTITION BY RANGE (created_at);
CREATE TABLE events_2026_07 PARTITION OF events
FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');
CREATE TABLE events_2026_08 PARTITION OF events
FOR VALUES FROM ('2026-08-01') TO ('2026-09-01');Sekarang setiap INSERT yang created_at-nya jatuh pada Juli akan otomatis dirutekan ke events_2026_07, dan yang Agustus ke events_2026_08. Aplikasi tidak perlu tahu partisi mana yang dipakai.
PARTITION BY LIST mempartisi berdasarkan nilai enumerasi — kategori, region, atau status. Cocok untuk data yang dikelompokkan secara natural:
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
region TEXT NOT NULL,
total NUMERIC(12,2) NOT NULL
) PARTITION BY LIST (region);
CREATE TABLE orders_jakarta PARTITION OF orders
FOR VALUES IN ('jakarta');
CREATE TABLE orders_bandung PARTITION OF orders
FOR VALUES IN ('bandung');
CREATE TABLE orders_lainnya PARTITION OF orders
DEFAULT;Perhatikan partisi DEFAULT yang menampung semua nilai yang tidak cocok — pengaman agar insert dengan region baru tidak gagal. Tapi gunakan dengan hati-hati: tanpa default, insert nilai di luar partisi akan error — yang justru bisa menjadi signal untuk menambah partisi baru.
PARTITION BY HASH membagi baris secara merata ke sejumlah partisi berdasarkan hash dari kolom kunci. Tidak ada makna bisnis pada tiap partisi — tujuannya murni membagi beban:
CREATE TABLE user_sessions (
id UUID NOT NULL,
user_id UUID NOT NULL,
session_data JSONB NOT NULL
) PARTITION BY HASH (user_id);
CREATE TABLE user_sessions_0 PARTITION OF user_sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE user_sessions_1 PARTITION OF user_sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE user_sessions_2 PARTITION OF user_sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE user_sessions_3 PARTITION OF user_sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 3);Empat partisi membagi data berdasar hash user_id dengan modulus 4. Setiap partisi berisi kurang lebih seperempat data — cocok untuk beban read/write yang merata dan tidak punya kolom dengan kelompok natural.
Note
Ringkasan pemilihan strategi: Range untuk time-series dan rentang numerik, List untuk nilai enumerasi (region, kategori), Hash untuk mendistribusikan beban merata tanpa makna bisnis pada partisi. Hampir semua data logis di produksi memakai Range.
Partition pruning adalah keajaiban yang membuat partitioning terasa seperti cheat: PostgreSQL memeriksa kondisi WHERE, lalu memotong eksekusi ke hanya partisi yang relevan. Partisi lain tidak disentuh sama sekali.
SELECT COUNT(*)
FROM events
WHERE created_at >= '2026-08-01'
AND created_at < '2026-08-15';Karena kolom partisi created_at disaring dengan rentang, PostgreSQL tahu baris hanya bisa berada di events_2026_08. Partisi lain tidak di-scan — query berjalan seperti menyentuh tabel kecil.
Bandingkan dengan query tanpa filter pada kolom partisi:
SELECT COUNT(*) FROM events;Query ini harus menyentuh semua partisi — tentu saja. Inilah kenapa partitioning harus selaras dengan pola query: kolom partisi harus selalu ikut disaring di WHERE, atau pruning tidak terjadi.
Warning
Partition pruning hanya efektif jika kondisi WHERE memakai kolom partisi dengan operator yang bisa dipangkas (range, equality). Ekspresi seperti WHERE created_at::DATE >= '2026-08-01' bisa menggagalkan pruning karena kolom dibungkus fungsi. Tulis kondisi pada kolom mentahnya agar planner bisa memotong partisi.
EXPLAIN
SELECT COUNT(*)
FROM events
WHERE created_at >= '2026-08-01'
AND created_at < '2026-08-15';Output akan menampilkan bagian seperti:
Append
Subplans Removed: 1
-> Seq Scan on events_2026_08 events_1Baris Subplans Removed menunjukkan berapa partisi yang diabaikan. Jika hasilnya 0, berarti tidak ada pruning — kembali periksa bentuk ekspresi WHERE.
Kelebihan operasional partitioning terlihat saat menambah dan menghapus periode data:
CREATE TABLE events_2026_09 PARTITION OF events
FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');DROP TABLE events_2026_05;Inti yang harus dibawa pulang:
WHERE memakai kolom partisi.DROP PARTITION jauh lebih cepat daripada DELETE untuk data lama.Di episode 19 selanjutnya, kita menyiapkan kesiapan produksi: Backup, Replication & PgBouncer — mulai dari logical backup dengan pg_dump dan pg_restore, physical backup dengan WAL untuk Point-In-Time Recovery, physical streaming dan logical replication, hingga connection pooling dengan PgBouncer.