Sebelum declarative partitioning hadir di PostgreSQL 10, komunitas sudah puluhan tahun memecah tabel besar — dengan mekanisme yang sama sekali berbeda bernama table inheritance. Sebuah tabel anak dibuat dengan klausa INHERITS (induk) untuk mewarisi kolom induknya, dan query dari induk otomatis menggabungkan baris semua anaknya. Pola ini pernah menjadi teknik partitioning de facto: batas partisi ditiru lewat CHECK constraint manual, routing ditiru lewat aturan aplikasi atau trigger. Mekanisme itu masih hidup dan sah di PostgreSQL 17, tetapi karakternya berbeda dalam hal-hal yang krusial. Chapter ini membedah inheritance apa adanya — routing manual, constraint yang tidak menyebar penuh, keunikan yang tidak dijaga lintas tabel — lalu membandingkannya langsung dengan declarative partitioning supaya jelas mengapa paradigma baru menggantikannya.

Anatomi INHERITS #

Tabel induk log_lama dideklarasikan seperti tabel biasa — tanpa PARTITION BY. Tabel anak dibuat dengan INHERITS (log_lama) dan mewarisi seluruh kolom induknya; CHECK pada anak berperan sebagai “batas partisi” versi lama:

sql buat-tabel.sql
-- Mekanisme lama: tabel anak mewarisi struktur induk lewat INHERITS.
CREATE TABLE log_lama (
    id      bigint NOT NULL,
    tanggal date NOT NULL,
    kota    text NOT NULL
);

-- Anak "Januari" dengan CHECK sebagai batas manual ala partitioning lama.
CREATE TABLE log_lama_januari (
    CHECK (tanggal >= DATE '2026-01-01' AND tanggal < DATE '2026-02-01')
) INHERITS (log_lama);

-- INSERT ke induk: TIDAK di-routing, baris menetap di heap induk sendiri.
INSERT INTO log_lama VALUES
    (1, DATE '2026-01-15', 'Jakarta');

-- INSERT ke anak harus ditujukan secara eksplisit.
INSERT INTO log_lama_januari VALUES
    (2, DATE '2026-01-20', 'Bandung');

-- SELECT dari induk menggabungkan baris induk + semua anak.
SELECT id, tanggal, kota, tableoid::regclass AS lokasi_fisik
FROM log_lama
ORDER BY id;

-- ONLY membatasi pembacaan ke heap induk saja.
SELECT id, tanggal, kota FROM ONLY (log_lama) ORDER BY id;

Output yang Dihasilkan:

text
CREATE TABLE
CREATE TABLE
INSERT 0 1
INSERT 0 1
 id |  tanggal   |  kota   |   lokasi_fisik   
----+------------+---------+------------------
  1 | 2026-01-15 | Jakarta | log_lama
  2 | 2026-01-20 | Bandung | log_lama_januari
(2 rows)

 id |  tanggal   |  kota   
----+------------+---------
  1 | 2026-01-15 | Jakarta
(1 row)

Dua INSERT di file itu memperlihatkan perbedaan paling mendasar. INSERT INTO log_lama dengan tanggal 15 Januari tidak di-routing — baris menetap di heap induk sendiri meskipun tabel anak “Januari” ada, karena inheritance tidak mengenal routing. Baris kedua harus ditujukan eksplisit ke log_lama_januari. Query dari induk tetap mengembalikan keduanya: PostgreSQL menggabungkan heap induk dengan heap semua anaknya (kolom lokasi_fisik memperlihatkan asal masing-masing). Kata kunci ONLY (log_lama) membatasi pembacaan ke heap induk saja — barisan satu baris, tanpa baris anak.

Constraint yang Menyebar Sebagian #

CHECK constraint adalah satu-satunya jenis constraint yang benar-benar diwariskan anak-anaknya. Constraint baru yang ditambahkan ke induk menyebar ke anak yang sudah ada, dan anak yang lahir kemudian mewarisinya sejak dibuat:

-- CHECK baru pada induk menyebar ke anak yang sudah ada...
ALTER TABLE log_lama ADD CONSTRAINT ck_kota_terisi
    CHECK (kota <> '');

-- ...dan diwarisi anak yang dibuat setelahnya.
CREATE TABLE log_lama_februari (
    CHECK (tanggal >= DATE '2026-02-01' AND tanggal < DATE '2026-03-01')
) INHERITS (log_lama);

SELECT conrelid::regclass AS tabel, conname AS constraint
FROM pg_constraint
WHERE conrelid IN ('log_lama'::regclass,
                   'log_lama_januari'::regclass,
                   'log_lama_februari'::regclass)
ORDER BY conrelid::regclass::text, conname;

Output yang Dihasilkan:

text
ALTER TABLE
CREATE TABLE
       tabel       |           constraint            
-------------------+---------------------------------
 log_lama          | ck_kota_terisi
 log_lama_februari | ck_kota_terisi
 log_lama_februari | log_lama_februari_tanggal_check
 log_lama_januari  | ck_kota_terisi
 log_lama_januari  | log_lama_januari_tanggal_check
(5 rows)

Daftar pg_constraint menunjukkan ck_kota_terisi hadir di ketiga tabel: ditambahkan sekali ke induk, tersalin ke anak Januari yang sudah ada, lalu diwarisi anak Februari saat diciptakan. Di titik ini inheritance terlihat cukup meyakinkan — tetapi batas “partisi” berupa CHECK hanya menggambarkan niat, bukan aturan routing: baris 15 Januari tetap sah menetap di heap induk karena induk tidak memiliki constraint tanggal apa pun.

PRIMARY KEY Tidak Dijaga Lintas Tabel #

Jenis constraint lain tidak diwariskan — dan itu mengubah maknanya secara diam-diam. PRIMARY KEY atau UNIQUE yang dipasang di induk hanya menjaga baris heap induk sendiri:

-- PRIMARY KEY pada induk hanya menjaga heap induk sendiri.
ALTER TABLE log_lama ADD PRIMARY KEY (id);

-- id = 1 sudah ada di induk; baris dengan id sama di anak tetap diterima.
INSERT INTO log_lama_januari VALUES
    (1, DATE '2026-01-25', 'Medan');

SELECT id, tanggal, kota, tableoid::regclass AS lokasi_fisik
FROM log_lama
WHERE id = 1
ORDER BY tableoid::regclass::text;

Output yang Dihasilkan:

text
ALTER TABLE
INSERT 0 1
 id |  tanggal   |  kota   |   lokasi_fisik   
----+------------+---------+------------------
  1 | 2026-01-15 | Jakarta | log_lama
  1 | 2026-01-25 | Medan   | log_lama_januari
(2 rows)

ALTER TABLE log_lama ADD PRIMARY KEY (id) berhasil dijalankan tanpa keluhan, lalu INSERT baris ber-id = 1 ke anak diterima tanpa error — padahal id = 1 sudah ada di heap induk. Hasil akhirnya dua baris id = 1 terbaca sekaligus dari induk. Penegakan unik lintas induk-anak memang tidak ada: masing-masing tabel punya struktur penyimpanan dan index sendiri, dan tidak ada mekanisme yang menggabungkannya untuk pemeriksaan keunikan. Index di induk juga tidak menjalar ke anak — setiap index harus dibuat per tabel.

DML dari Induk Menjangkau Anak #

Meski tulis ke induk tidak di-routing, pembacaan dan perubahan data lewat induk menjangkau anak secara penuh. UPDATE dan DELETE tanpa ONLY bekerja atas gabungan induk + anak:

-- UPDATE dari induk ikut menjangkau baris di tabel anak.
UPDATE log_lama SET kota = 'Depok' WHERE id = 2;
SELECT id, kota FROM log_lama_januari ORDER BY id;

-- ONLY membatasi DELETE ke heap induk saja.
DELETE FROM ONLY (log_lama) WHERE id = 1;
SELECT count(*) AS sisa_di_induk FROM ONLY (log_lama);
SELECT count(*) AS sisa_di_anak FROM log_lama_januari;

Output yang Dihasilkan:

text
UPDATE 1
 id | kota  
----+-------
  1 | Medan
  2 | Depok
(2 rows)

DELETE 1
 sisa_di_induk 
---------------
             0
(1 row)

 sisa_di_anak 
--------------
            2
(1 row)

UPDATE ... WHERE id = 2 mengubah baris yang fisiknya hidup di log_lama_januari — baris itu kini ber-kota Depok. Sebaliknya DELETE FROM ONLY (log_lama) WHERE id = 1 hanya menjatuhkan baris milik heap induk (DELETE 1), meninggalkan kedua baris anak tetap utuh. Perilaku gabung-untuk-baca ini menjadikan inheritance tetap berguna untuk query analitik lintas cabang, namun kombinasi “baca gabungan, tulis manual” inilah yang membuat pola partitioning lama mudah bocor: satu INSERT yang salah alamat cukup untuk membuat data tak terlihat dari query anak.

Perbandingan Langsung dengan Declarative #

Tabel yang sama, data yang sama, mekanisme yang berbeda — pembandingnya kini satu layar:

-- Declarative partitioning: routing INSERT otomatis tanpa campur tangan.
CREATE TABLE log_baru (
    id      bigint NOT NULL,
    tanggal date NOT NULL,
    kota    text NOT NULL
) PARTITION BY RANGE (tanggal);

CREATE TABLE log_baru_2026_01 PARTITION OF log_baru
    FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');

INSERT INTO log_baru VALUES
    (1, DATE '2026-01-15', 'Jakarta'),
    (2, DATE '2026-01-20', 'Bandung');

SELECT id, tanggal, kota, tableoid::regclass AS lokasi_fisik
FROM log_baru
ORDER BY id;

-- Induk deklaratif murni metadata; induk inheritance menyimpan baris sendiri.
SELECT 'log_baru (declarative)' AS mekanisme,
       pg_relation_size('log_baru') AS ukuran_induk
UNION ALL
SELECT 'log_lama (inheritance)', pg_relation_size('log_lama');

Output yang Dihasilkan:

text
CREATE TABLE
CREATE TABLE
INSERT 0 2
 id |  tanggal   |  kota   |   lokasi_fisik   
----+------------+---------+------------------
  1 | 2026-01-15 | Jakarta | log_baru_2026_01
  2 | 2026-01-20 | Bandung | log_baru_2026_01
(2 rows)

       mekanisme        | ukuran_induk 
------------------------+--------------
 log_baru (declarative) |            0
 log_lama (inheritance) |         8192
(2 rows)

Pada log_baru, dua INSERT ditujukan ke induk dan keduanya otomatis mendarat di log_baru_2026_01 — routing, penolakan baris tanpa partisi, pruning, index terpartisi, dan DETACH/ATTACH semuanya tersedia sebagai satu paket. Ukuran penutup merangkum perbedaan arsitekturnya: induk declarative berukuran 0 byte karena murni metadata, sedangkan induk inheritance menyimpan barisnya sendiri (8192 byte) sehingga selamanya menjadi “partisi tambahan” yang tidak pernah kosong secara struktural.

AspekInheritanceDeclarative Partitioning
Routing INSERTmanual (aplikasi/trigger)otomatis oleh server
Batas partisiCHECK manual, tidak mengikat routingFOR VALUES mengikat dan ditolak bila bertabrakan
Constraint unik/PKper tabel, tidak lintasper partisi, wajib memuat partition key
Indexdibuat per tabel manualsatu perintah di induk menjalar
Pruningtidak ada (selalu baca semua anak)otomatis, statis maupun runtime
Kegunaan saat inihierarki tabel analitik, tabel kosong sebagai tampilan unionpemecahan tabel besar standar

Kapan inheritance masih dipakai secara jujur di era PostgreSQL 17? Kasusnya sempit: hierarki tabel yang memang ingin dibaca gabungan tetapi ditulis terpisah — misalnya tabel induk kosong sebagai “pintu query” atas banyak tabel cabang warisan sistem lama, atau pengujian skema yang mengganti sebagian tabel. Untuk kebutuhan partitioning apa pun — rentang waktu, daftar nilai, hash — pilihan yang benar hari ini adalah declarative. Dengan seluruh perangkat section ini di tangan, dari konsep hingga pemeliharaan, kita siap naik ke pertanyaan yang lebih besar: bagaimana menyusun strategi skala menyeluruh untuk PostgreSQL — topik penutup section.

Referensi #

Berkas Sumber

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

buat-tabel.sql

Unduh berkas
sql
-- Mekanisme lama: tabel anak mewarisi struktur induk lewat INHERITS.
CREATE TABLE log_lama (
    id      bigint NOT NULL,
    tanggal date NOT NULL,
    kota    text NOT NULL
);

-- Anak "Januari" dengan CHECK sebagai batas manual ala partitioning lama.
CREATE TABLE log_lama_januari (
    CHECK (tanggal >= DATE '2026-01-01' AND tanggal < DATE '2026-02-01')
) INHERITS (log_lama);

-- INSERT ke induk: TIDAK di-routing, baris menetap di heap induk sendiri.
INSERT INTO log_lama VALUES
    (1, DATE '2026-01-15', 'Jakarta');

-- INSERT ke anak harus ditujukan secara eksplisit.
INSERT INTO log_lama_januari VALUES
    (2, DATE '2026-01-20', 'Bandung');

-- SELECT dari induk menggabungkan baris induk + semua anak.
SELECT id, tanggal, kota, tableoid::regclass AS lokasi_fisik
FROM log_lama
ORDER BY id;

-- ONLY membatasi pembacaan ke heap induk saja.
SELECT id, tanggal, kota FROM ONLY (log_lama) ORDER BY id;

constraint-warisan.sql

Unduh berkas
sql
-- CHECK baru pada induk menyebar ke anak yang sudah ada...
ALTER TABLE log_lama ADD CONSTRAINT ck_kota_terisi
    CHECK (kota <> '');

-- ...dan diwarisi anak yang dibuat setelahnya.
CREATE TABLE log_lama_februari (
    CHECK (tanggal >= DATE '2026-02-01' AND tanggal < DATE '2026-03-01')
) INHERITS (log_lama);

SELECT conrelid::regclass AS tabel, conname AS constraint
FROM pg_constraint
WHERE conrelid IN ('log_lama'::regclass,
                   'log_lama_januari'::regclass,
                   'log_lama_februari'::regclass)
ORDER BY conrelid::regclass::text, conname;

pk-tidak-lintas.sql

Unduh berkas
sql
-- PRIMARY KEY pada induk hanya menjaga heap induk sendiri.
ALTER TABLE log_lama ADD PRIMARY KEY (id);

-- id = 1 sudah ada di induk; baris dengan id sama di anak tetap diterima.
INSERT INTO log_lama_januari VALUES
    (1, DATE '2026-01-25', 'Medan');

SELECT id, tanggal, kota, tableoid::regclass AS lokasi_fisik
FROM log_lama
WHERE id = 1
ORDER BY tableoid::regclass::text;

update-dari-induk.sql

Unduh berkas
sql
-- UPDATE dari induk ikut menjangkau baris di tabel anak.
UPDATE log_lama SET kota = 'Depok' WHERE id = 2;
SELECT id, kota FROM log_lama_januari ORDER BY id;

-- ONLY membatasi DELETE ke heap induk saja.
DELETE FROM ONLY (log_lama) WHERE id = 1;
SELECT count(*) AS sisa_di_induk FROM ONLY (log_lama);
SELECT count(*) AS sisa_di_anak FROM log_lama_januari;

vs-deklaratif.sql

Unduh berkas
sql
-- Declarative partitioning: routing INSERT otomatis tanpa campur tangan.
CREATE TABLE log_baru (
    id      bigint NOT NULL,
    tanggal date NOT NULL,
    kota    text NOT NULL
) PARTITION BY RANGE (tanggal);

CREATE TABLE log_baru_2026_01 PARTITION OF log_baru
    FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');

INSERT INTO log_baru VALUES
    (1, DATE '2026-01-15', 'Jakarta'),
    (2, DATE '2026-01-20', 'Bandung');

SELECT id, tanggal, kota, tableoid::regclass AS lokasi_fisik
FROM log_baru
ORDER BY id;

-- Induk deklaratif murni metadata; induk inheritance menyimpan baris sendiri.
SELECT 'log_baru (declarative)' AS mekanisme,
       pg_relation_size('log_baru') AS ukuran_induk
UNION ALL
SELECT 'log_lama (inheritance)', pg_relation_size('log_lama');