PostgreSQL → Partitioning
Sumber: Luca Ferrari, Enrico Pirozzi - Learn PostgreSQL Use, manage, and build secure and scalable databases with PostgreSQL 16 (2023) - Chapter 9
Last updated
Sumber: Luca Ferrari, Enrico Pirozzi - Learn PostgreSQL Use, manage, and build secure and scalable databases with PostgreSQL 16 (2023) - Chapter 9
Last updated
Pertama-tama, mari kita coba memahami mengapa kita harus melakukan partisi data. Kita harus mulai dengan mengatakan bahwa ukuran / size database itu akan selalu bertambah. Oleh karena itu, mungkin saja sebuah database, setelah beberapa bulan growth, dapat mencapai ukuran gigabyte, terabyte, atau bahkan petabyte.
Hal lain yang harus selalu kita ingat adalah bahwa tidak semua tabel itu grow / tumbuh pada tingkat yang sama. Beberapa tabel lebih besar dari tabel lain dan beberapa index juga lebih besar dari index lainnya.
Kita juga perlu tahu bahwa ada bagian dari RAM server kita, yang digunakan bersama oleh semua proses PostgreSQL, yang digunakan untuk mengelola data yang ada dalam tabel. Bagian dari RAM server ini disebut shared_buffers
.
Cara kerja PostgreSQL adalah sebagai berikut:
Data diambil dari hard disk.
Data ditempatkan di shared_buffers
.
Data diproses di shared_buffers
.
Data diunduh ke disk.
Biasanya, di server khusus hanya untuk PostgreSQL, ukuran shared_buffers
adalah sekitar sepertiga atau seperempat dari total RAM server. Berikut referensi untuk mengatur beberapa parameter konfigurasi PostgreSQL (termasuk ukuran yang disarankan untuk shared_buffers
) .
Ketika volume data dari sebuah tabel grow secara berlebihan dibandingkan dengan ukuran shared_buffers
, ada kemungkinan kinerja akan menurun. Dalam hal ini, partitioning data dapat membantu kita. Partitioning berarti memecah tabel yang sangat besar menjadi tabel-tabel yang lebih kecil. Program client akan berpikir bahwa server masih hanya memiliki satu tabel, tetapi memiliki tabel yang lebih kecil juga berarti memiliki index yang lebih kecil yang memiliki peluang lebih tinggi untuk tetap berada di memori yang akan meningkatkan kinerja data. Selain itu, memiliki tabel yang lebih kecil berarti bahwa proses vacuum
bekerja pada tabel yang lebih kecil, yang meminimalkan waktu eksekusi proses vacuum
. Ketika menjalankan vacuum full
, ruang disk yang digunakan oleh tabel menjadi dua kali lipat, oleh karena itu memiliki banyak tabel kecil daripada satu tabel besar secara signifikan mengurangi dampak dari masalah ini. Partitioning data dapat dilakukan dengan dua cara:
Menggunakan table inheritance (satu-satunya cara yang mungkin untuk versi PostgreSQL < 10)
Menggunakan declarative partitioning (cara terbaik mulai dari versi 10)
Setelah mengetahui kapan disarankan untuk melakukan partisi data, mari kita lihat jenis-jenis partitioning tabel apa saja yang mungkin. PostgreSQL mendukung tiga jenis declarative partitioning:
Range partitioning
List partitioning
Hash partitioning
Sebelum kita menjelaskan ketiga metode ini secara detail, mari kita bahas terlebih dahulu tentang vacuum
.
Dalam PostgreSQL, VACUUM
dan VACUUM FULL
adalah dua perintah penting untuk menjaga kinerja dan efisiensi database. Keduanya digunakan untuk membersihkan dead tuples (baris yang sudah tidak berlaku lagi karena operasi UPDATE atau DELETE), namun memiliki perbedaan mendasar dalam cara kerja dan dampaknya terhadap sistem.
PostgreSQL menggunakan sistem penyimpanan Multi-Version Concurrency Control (MVCC), yang memungkinkan banyak transaksi untuk membaca data tanpa saling mengganggu. Setiap kali ada operasi UPDATE
atau DELETE
, baris lama tidak dihapus langsung, tapi ditandai sebagai dead tuple. Baris baru dibuat dalam kasus UPDATE
.
Karena baris lama tetap disimpan, database akan membengkak seiring waktu. Di sinilah VACUUM
berperan.
Fungsi:
Membersihkan dead tuples yang sudah tidak dibutuhkan.
Membebaskan ruang agar bisa digunakan kembali oleh query baru.
Memperbarui statistik internal PostgreSQL (jika pakai ANALYZE
).
Menghindari pembengkakan ukuran tabel dan index.
VACUUM FULL
melakukan semua yang dilakukan oleh VACUUM
, ditambah dengan:
Mengembalikan ruang kosong ke sistem operasi (bukan cuma untuk reuse internal).
Memampatkan tabel (tabel di-rewrite dari nol).
Mengurangi ukuran file tabel secara fisik.
Namun, VACUUM FULL
mengunci tabel selama proses berlangsung, sehingga tidak cocok untuk tabel yang sering diakses secara langsung di sistem produksi.
Digunakan untuk reclaim space ketika tabel sudah terlalu besar karena banyak UPDATE
/DELETE
.
Mengatasi pembengkakan file .rel
yang tidak bisa diatasi oleh VACUUM
biasa.
Perlu Diperhatikan:
Lebih lambat dari VACUUM
.
Eksklusif lock (tidak bisa diakses selama proses berlangsung).
Harus digunakan secara terencana/offline maintenance.
Perbandingan VACUUM vs VACUUM FULL
Membersihkan dead tuples
✅
✅
Mengembalikan ruang ke OS
❌ (hanya internal reuse)
✅ (kompak file fisik)
Lock Tabel
Tidak eksklusif
Eksklusif (tidak bisa diakses)
Kecepatan
Cepat
Lebih lambat
Risiko gangguan sistem
Minimal
Tinggi (harus maintenance)
Ukuran file tabel
Tidak mengecil
Ukuran fisik bisa mengecil
PostgreSQL juga memiliki autovacuum daemon yang secara otomatis melakukan VACUUM
pada tabel yang banyak berubah. Ini mengurangi kebutuhan untuk menjalankan VACUUM
manual, tapi tidak melakukan VACUUM FULL
secara otomatis.
Konfigurasi penting:
autovacuum_vacuum_threshold
autovacuum_vacuum_scale_factor
autovacuum_naptime
Tips Praktis
Gunakan VACUUM ANALYZE
secara berkala untuk memperbarui statistik query planner.
Untuk mengidentifikasi bloat, gunakan ekstensi seperti pgstattuple
atau query dari pg_stat_user_tables
.
Jangan sering-sering pakai VACUUM FULL
kecuali benar-benar perlu.
“Ketika menjalankan vacuum full, ruang disk yang digunakan oleh tabel menjadi dua kali lipat.” benar dalam konteks proses sementara. Berikut adalah penjelasan makna teknis dan mengapa ini terjadi:
VACUUM FULL
tidak membersihkan data pada tempatnya (in-place).
PostgreSQL akan:
Membuat salinan baru dari tabel di file baru.
Menyalin hanya data yang masih aktif/valid (tanpa dead tuples) ke salinan baru.
Setelah selesai, PostgreSQL akan:
Menghapus file lama.
Mengganti pointer tabel ke file baru yang bersih dan lebih kecil.
Apa yang Terjadi di Disk? Misalkan kamu punya tabel:
Tabel lama
1 GB
Lalu kamu menjalankan:
Selama proses berlangsung, PostgreSQL akan:
Copy tabel lama → tabel baru
1 GB tambahan
Total sementara
2 GB
Setelah selesai (tabel lama dihapus)
~1 GB (lebih kecil karena sudah dibersihkan)
Selama VACUUM FULL
berlangsung, dua versi tabel akan ada di disk:
File tabel lama (yang masih ada).
File tabel baru (yang sedang dibangun).
Setelah selesai, file lama dihapus, dan hanya file bersih yang tersisa.
Catatan
Jika tabel besar (misalnya 50 GB), dan disk kamu hanya tersisa 30 GB, maka VACUUM FULL
bisa gagal karena tidak cukup ruang untuk membuat salinan baru.
Ini kenapa VACUUM FULL
harus dilakukan dengan hati-hati di production system.
Kalau kamu ingin alternatif yang lebih hemat disk, bisa gunakan:
VACUUM
reguler (tidak reclaim ke OS tapi cepat).
pg_repack
(tool eksternal yang melakukan vacuum-like proses tanpa full lock dan dengan efisien ruang disk). Mau dijelaskan juga?
Sebelum lanjut, Kita akan membuat docker container bernama belajar-partitioning, seperti yang ditunjukkan pada file docker compose bawah ini:
Mari kita lihat contoh definisi range partitioning. Misalkan kita memiliki tabel ini:
Tabel sebelum range partitioning
field_date
field_value
2023-03-01
1
2023-03-02
10
2023-04-01
12
2023-04-15
1
Sekarang anggaplah kita ingin membagi tabel ini menjadi dua tabel. Tabel pertama (TABEL A) akan berisi semua record dengan nilai field_date
antara 2023-03-01 dan 2023-03-31, dan tabel kedua (TABEL B) akan berisi semua record dengan nilai field_date
antara 2023-04-01 dan 2023-04-30. Jadi, tujuan kita adalah memiliki dua tabel sebagai berikut:
Tabel A
field_date
field_value
2023-03-01
1
2023-03-02
10
Tabel B
field_date
field_value
2023-04-01
12
2023-04-15
1
Apa yang telah kita lihat adalah contoh partitioning berdasarkan range. Ini berguna ketika kita memiliki tabel besar di mana data dapat dibagi berdasarkan rentang waktu, misalnya, omset, tabel audit, atau tabel log.
Dalam list partitioning, tabel akan dipartisi menggunakan daftar nilai.
Mari kita lihat contoh definisi list partitioning. Misalkan kita memiliki tabel ini:
Tabel sebelum list partitioning
field_state
field_city
United States
Washington
United States
San Francisco
Italy
Rome
Japan
Tokyo
Misalkan sekarang kita ingin membagi tabel ini menjadi n tabel, dengan satu tabel untuk setiap negara bagian. Tabel pertama (TABEL A) akan berisi semua record dengan nilai field_state
sama dengan United States, tabel kedua (TABEL B) akan berisi semua record dengan nilai field_state
sama dengan Italy, dan tabel ketiga (TABEL C) akan berisi record dengan nilai field_state
sama dengan Japan. Jadi, tujuan kita adalah memiliki tiga tabel sebagai berikut:
Tabel A
field_state
field_city
United States
Washington
United States
San Francisco
Tabel B
field_state
field_city
Italy
Rome
Tabel C
field_state
field_city
Japan
Tokyo
Ini adalah contoh partitioning berdasarkan list. Ini berguna ketika kita memiliki tabel besar di mana data dapat dibagi oleh satu field, seperti field kota atau negara bagian dalam buku telepon, atau dalam daftar pelanggan.
Dengan menggunakan hash partitioning, tabel akan dipartisi menggunakan nilai hash untuk membagi data ke dalam tabel yang berbeda. Mari kita lihat contoh hash partitioning. Misalkan kita memiliki tabel ini:
Tabel sebelum hash partitioning
field_date
field_value
2023-03-01
1
2023-03-02
1
2023-04-01
2
2023-04-15
2
Misalkan sekarang kita memiliki fungsi hash yang mengubah tanggal menjadi nilai hash, misalnya:
hash(1) = 1
hash(1) = 1
hash(2) = 0
hash(2) = 0
Jadi, setelah proses partitioning, kita akan memiliki dua tabel:
Tabel A
field_date
field_value
2023-03-01
1
2023-03-02
1
Tabel B
field_date
field_value
2023-04-01
2
2023-04-15
2
Ini adalah contoh partitioning berdasarkan hash.
Di bagian selanjutnya kita akan melihat bagaimana PostgreSQL mengimplementasikan list, range, dan hash partitioning, tetapi sebelum itu, mari kita luangkan waktu untuk berbicara tentang table inheritance.
Topik lain yang harus kita lihat adalah table inheritance. PostgreSQL menggunakan konsep inheritance dari database ke objek. Konsepnya sangat sederhana dan dapat diringkas sebagai berikut: misalkan kita memiliki dua tabel, TABEL A dan TABEL B. Jika kita mendefinisikan TABEL A sebagai tabel parent dan TABEL B sebagai tabel child, ini berarti bahwa semua record di TABEL B akan dapat diakses dari TABEL A.
Mari sekarang kita coba memberikan contoh dari apa yang baru saja kita jelaskan:
Mari kita definisikan dua tabel.
Tabel pertama, tabel parent, didefinisikan sebagai berikut:
Dan tabel kedua, tabel child, didefinisikan sebagai berikut:
Tabel child mewarisi semua field dari tabel parent. Tabel parent seperti yang terlihat di sini:
Dan untuk detail lebih lanjut, kita bisa gunakan perintah \d+
Dalam tabel terakhir ini, kita dapat melihat bahwa table_b
adalah tabel child dari table_a
.
Mari kita lakukan hal yang sama untuk tabel bernama table_b
:
Di sini, kita dapat melihat bahwa table_b
adalah tabel child dari table_a
.
Sekarang mari kita lihat bagaimana kedua tabel ini berperilaku jika kita memasukkan, mengubah, atau menghapus data. Misalnya, mari kita lakukan beberapa insert sebagai berikut:
Mari kita lihat bagaimana data kita bereaksi jika kita mengeksekusi perintah select
:
Kita dapat melihat bahwa table_b
memiliki satu record.
Sekarang kita jalankan perintah berikut:
Tampaknya table_a
memiliki dua record. Ini terjadi karena tabel ini mewarisi atribut tabel lain. Jika kita mengeksekusi perintah SELECT
pada tabel parent, kita akan melihat semua record yang dimiliki oleh tabel parent dan semua record yang dimiliki oleh tabel child.
Jika kita ingin melihat semua record yang hanya dimiliki oleh table_a
, kita harus menggunakan klausa ONLY
, seperti yang terlihat di sini:
Mari kita lihat apa yang terjadi jika kita UPDATE
beberapa record, misalnya, jika kita mengeksekusi yang berikut:
Kita melakukan operasi update pada table_a
, tetapi update ini secara fisik dilakukan pada table_b
melalui inheritance tabel, seperti yang dapat kita lihat di sini:
Hal yang sama terjadi jika kita menggunakan statement delete
sebagai berikut:
Di sini, sekali lagi, operasi delete yang dilakukan pada table_a
memiliki efek pada table_b
; seperti yang dapat kita lihat di sini, table_a
akan memiliki record ini:
Dan table_b
sekarang tidak akan memiliki record:
Di PostgreSQL, inheritance menyebarkan operasi yang dilakukan pada tabel parent ke tabel child.
Untuk menyimpulkan topik inheritance, kita perlu membahas cara menghapus tabel. Jika kita ingin menghapus tabel child, misalnya, untuk drop table_b
, kita harus menjalankan statement berikut:
Jika kita ingin drop tabel parent dan semua tabel child yang terhubung dengannya, kita harus menjalankan yang berikut:
Meskipun inheritance telah digunakan dan masih dapat digunakan untuk mengimplementasikan partitioning tabel, sejak versi 10 declarative partitioning telah menjadi metode yang lebih disukai. Kita membahas declarative partitioning di bagian selanjutnya.
Di bagian ini, kita akan berbicara tentang declarative partitioning. Ini telah tersedia di PostgreSQL sejak versi 10, tetapi kinerjanya telah meningkat di versi yang lebih baru. Kita sekarang akan melihat contoh partitioning berdasarkan range dan contoh partitioning berdasarkan list.
Dalam contoh pertama declarative partitioning, kita akan menggunakan contoh yang sama yang kita lihat ketika kita memperkenalkan partitioning menggunakan inheritance. Kita akan melihat bahwa hal-hal menjadi jauh lebih sederhana menggunakan metode declarative partitioning:
Sekarang mari kita buat tabel parent kita:
Seperti yang dapat kita lihat dari contoh sebelumnya, kita harus mendefinisikan jenis partitioning apa yang ingin kita terapkan. Dalam hal ini, ini adalah LIST PARTITIONING
. Hal penting lain yang perlu diperhatikan adalah bahwa field yang digunakan untuk mempartisi data harus menjadi bagian dari primary key.
Selanjutnya, mari kita definisikan tabel child:
Dalam contoh ini, kita telah membuat tiga tabel child yang mewarisi dari tabel parent. Tabel part_tags_level_0
akan berisi semua record dengan level
sama dengan 0, part_tags_level_1
akan berisi semua record dengan level
sama dengan 1, dan part_tags_level_2
akan berisi semua record dengan level
sama dengan 2.
Mari kita coba memasukkan beberapa data:
Mari kita lihat apa yang terjadi jika kita menjalankan perintah SELECT
pada tabel parent:
Tabel parent berisi semua data dari semua tabel child.
Sekarang mari kita lihat apa yang terjadi jika kita menjalankan perintah SELECT
pada tabel child:
Setiap tabel child berisi record yang sesuai dengan level yang ditentukan dalam perintah CREATE TABLE
.
Jika kita mencoba memasukkan record dengan level yang tidak ditentukan, kita akan mendapatkan kesalahan:
Sekarang mari kita lihat contoh lain, kali ini menggunakan range partitioning.
Mari kita buat tabel parent kita:
Seperti yang dapat kita lihat dari contoh sebelumnya, kita harus menentukan jenis partitioning apa yang ingin kita terapkan. Dalam hal ini, ini adalah RANGE PARTITIONING
. Hal penting lain yang perlu diperhatikan adalah bahwa field yang digunakan untuk mempartisi data harus menjadi bagian dari primary key.
Selanjutnya, mari kita definisikan tabel child:
Dalam contoh ini, kita telah membuat dua tabel child yang mewarisi dari tabel parent. Tabel part_tags_date_01
akan berisi semua record dengan create_date
antara 2023-01-01 dan 2023-06-30, dan part_tags_date_02
akan berisi semua record dengan create_date
antara 2023-07-01 dan 2023-12-31.
Mari kita coba memasukkan beberapa data:
Mari kita lihat apa yang terjadi jika kita menjalankan perintah SELECT
pada tabel parent:
Tabel parent berisi semua data dari semua tabel child.
Sekarang mari kita lihat apa yang terjadi jika kita menjalankan perintah SELECT
pada tabel child:
Setiap tabel child berisi record yang sesuai dengan rentang yang ditentukan dalam perintah CREATE TABLE
.
Jika kita mencoba memasukkan record dengan tanggal yang tidak ditentukan, kita akan mendapatkan kesalahan:
Partition Maintenance
Jika kita ingin membuat / attach partisi baru ke parent table, kita bisa jalankan perintah:
Jika kita ingin detach / melepaskan partisi bisa jalankan perintah:
Default partition memastikan bahwa tidak ada data yang hilang; jika PostgreSQL tidak menemukan tabel child mana pun untuk menyimpan record, maka record tersebut disimpan di default partition.
Mari kita lihat contohnya:
Mari kita buat tabel parent kita:
Selanjutnya, mari kita definisikan tabel child kita:
Dalam contoh ini, kita telah membuat default partition.
Mari kita coba memasukkan beberapa data:
Mari kita lihat apa yang terjadi jika kita menjalankan perintah SELECT
pada tabel parent:
Tabel parent berisi semua data dari semua tabel child.
Sekarang mari kita lihat apa yang terjadi jika kita menjalankan perintah SELECT
pada tabel child:
Setiap tabel child berisi record yang sesuai dengan rentang yang ditentukan dalam perintah CREATE TABLE
.
Karena kita memiliki default partition, kita tidak lagi mendapatkan kesalahan jika kita mencoba memasukkan record dengan tanggal yang tidak ditentukan.
Tablespace adalah lokasi di sistem file tempat PostgreSQL menyimpan data. Secara default, PostgreSQL menyimpan semua data dalam satu tablespace. Namun, kita dapat membuat tablespace tambahan dan menyimpan data tabel yang berbeda di tablespace yang berbeda. Ini dapat berguna jika kita memiliki beberapa disk dan ingin menyebarkan data di berbagai disk.
Mari kita lihat contohnya:
Pertama, kita akan membuat dua tablespace baru:
Sekarang mari kita buat tabel parent kita:
Selanjutnya, mari kita definisikan tabel child kita dan menentukan tablespace tempat kita ingin menyimpannya:
Dalam contoh ini, tabel part_tags_date_01
akan disimpan di tablespace part_tags_date_01_space
, dan tabel part_tags_date_02
akan disimpan di tablespace part_tags_date_02_space
.
Mari kita coba memasukkan beberapa data:
Mari kita lihat apa yang terjadi jika kita menjalankan perintah SELECT
pada tabel parent:
Tabel parent berisi semua data dari semua tabel child.
Sekarang mari kita lihat apa yang terjadi jika kita menjalankan perintah SELECT
pada tabel child:
Setiap tabel child berisi record yang sesuai dengan rentang yang ditentukan dalam perintah CREATE TABLE
.
PostgreSQL mengelola index pada tabel yang dipartisi; jika kita membuat index pada tabel parent, index tersebut secara otomatis dibuat pada tabel child.
Mari kita lihat contohnya:
Mari kita buat tabel parent kita:
Selanjutnya, mari kita definisikan tabel child kita:
Mari kita buat index pada tabel parent:
Mari kita lihat index pada tabel parent:
Mari kita lihat index pada tabel child:
Seperti yang dapat kita lihat, PostgreSQL mengelola index pada tabel yang dipartisi.
Kita juga dapat menggunakan triggers untuk melakukan partitioning. Ini adalah metode yang sangat fleksibel, tetapi juga lebih lambat daripada declarative partitioning.
Mari kita lihat contohnya:
Mari kita buat tabel parent kita:
Selanjutnya, mari kita definisikan tabel child kita:
Sekarang kita akan membuat sebuah fungsi yang akan memutuskan tabel child mana yang akan menyimpan record:
Sekarang kita akan membuat trigger yang akan memanggil fungsi tersebut:
Mari kita coba memasukkan beberapa data:
Mari kita lihat apa yang terjadi jika kita menjalankan perintah SELECT
pada tabel parent:
Tabel parent tidak berisi data apa pun.
Sekarang mari kita lihat apa yang terjadi jika kita menjalankan perintah SELECT
pada tabel child:
Setiap tabel child berisi record yang sesuai dengan rentang yang ditentukan dalam trigger. Karena kita menggunakan trigger, kita mendapatkan kesalahan jika kita mencoba memasukkan record dengan tanggal yang tidak ditentukan.
Ada beberapa batasan pada partitioning:
Semua tabel child harus mewarisi dari tabel parent.
Field yang digunakan untuk mempartisi data harus menjadi bagian dari primary key.
Kita tidak dapat menggunakan UPDATE
untuk memindahkan baris dari satu partisi ke partisi lain. Kita harus menggunakan DELETE
dan INSERT
.
Kita tidak dapat menggunakan TRUNCATE
pada tabel parent. Kita harus menggunakan TRUNCATE
pada setiap tabel child.
Kita tidak dapat menggunakan FOREIGN KEY
pada tabel yang dipartisi.
Partitioning adalah teknik yang sangat berguna, tetapi tidak selalu diperlukan. Berikut adalah beberapa pedoman tentang kapan menggunakan partitioning:
Gunakan partitioning ketika tabel Anda sangat besar.
Gunakan partitioning ketika Anda memiliki query yang hanya mengakses sebagian kecil dari data.
Gunakan partitioning ketika Anda memiliki operasi yang perlu dilakukan pada sebagian kecil dari data.
Jangan gunakan partitioning jika tabel Anda kecil.
Jangan gunakan partitioning jika Anda memiliki query yang mengakses sebagian besar data.
Jangan gunakan partitioning jika Anda tidak memiliki operasi yang perlu dilakukan pada sebagian kecil dari data.
Kita telah melihat bagaimana kita dapat melakukan partisi data, dan, dalam beberapa kasus, bagaimana kita dapat menggunakan rules dan triggers yang telah dipelajari di bab sebelumnya untuk memungkinkan partitioning. Kita mulai dengan memperkenalkan konsep dasar partitioning, dan kemudian kita melihat kemungkinan yang ditawarkan PostgreSQL untuk mengimplementasikan partitioning.
Range partitioning adalah di mana tabel dibagi menjadi "interval." Interval tidak boleh tumpang tindih dan rentang didefinisikan melalui penggunaan field atau sekumpulan field. Untuk informasi lebih lanjut, lihat .
Untuk informasi lebih lanjut tentang partitioning, lihat .