Partisi tabel membagi tabel besar menjadi partisi fisik yang lebih kecil pada node database yang sama, meningkatkan kinerja kueri melalui pemangkasan partisi. Sharding melangkah lebih jauh dengan mendistribusikan partisi tersebut ke beberapa node database untuk memungkinkan scale-out horizontal. Anda bisa menggunakan partisi tanpa sharding — artikel ini merekomendasikan memulai dengan partisi terlebih dahulu dan menambahkan sharding hanya ketika penskalaan vertikal benar-benar sudah habis.
Artikel ini mengidentifikasi sinyal yang jelas: CPU atau I/O node utama secara konsisten di atas 70% meski sudah dioptimasi, tabel terbesar mencapai ratusan gigabyte dan terus tumbuh, isolasi data tenant adalah persyaratan arsitektur yang wajib, atau jendela backup dan restore sudah terlalu lama. Jika tidak ada kondisi tersebut yang berlaku, langkah yang tepat adalah pengindeksan yang lebih baik, optimasi kueri, atau read replica — bukan sharding.
Kolom distribusi menentukan apakah kueri hanya menyentuh satu shard atau menyebar ke semua shard. Aturan emas dari artikel ini: kueri yang memfilter pada kolom distribusi berjalan cepat; kueri yang tidak adalah operasi scatter-gather yang menyentuh setiap shard. Untuk ERP multi-tenant, tenant_id adalah pilihan yang jelas karena semua kueri dalam sesi tenant sudah memfilter berdasarkan kolom tersebut. Hindari kolom dengan distribusi nilai yang miring atau kolom yang sering berubah.
Sebagian besar aplikasi lebih banyak membaca — artikel ini mencatat bahwa 80–90% kueri adalah operasi baca. Menambahkan satu primary ditambah dua read replica melipattigakan kapasitas baca tanpa perubahan skema, cukup dengan mengarahkan kueri analitik, pembuatan laporan, dan endpoint API hanya-baca ke replica, serta penulisan ke primary. Ini hanya perubahan konfigurasi di framework seperti NestJS dengan TypeORM atau Prisma, dan memberikan ruang yang signifikan sebelum sharding diperlukan.
Sharding menghilangkan jaminan integritas referensial standar — foreign key dan join lintas shard tidak tersedia atau sangat mahal, sehingga integritas harus diterapkan di lapisan aplikasi. Transaksi yang mencakup beberapa shard memerlukan protokol transaksi terdistribusi, dan migrasi skema harus diterapkan ke semua shard secara bersamaan. Artikel ini juga memperingatkan bahwa migrasi dari single node ke kluster Citus adalah proses yang tidak sepele, dengan rekomendasi merencanakan 2–4 minggu pekerjaan migrasi untuk database besar.
PostgreSQL dapat menangani jutaan baris dan ribuan koneksi bersamaan pada satu node — sebagian besar aplikasi tidak pernah membutuhkan sharding. Tetapi ketika Anda memang melampaui ambang batas itu, keputusan sharding membentuk seluruh arsitektur Anda selama bertahun-tahun. Saya telah membangun sistem berbasis PostgreSQL untuk kasus penggunaan ERP di mana satu node database menangani semua data Commsult Indonesia dengan nyaman. Panduan ini mencakup spektrum dari partisi tabel (yang dapat dan harus Anda lakukan lebih awal) hingga sharding terdistribusi penuh dengan Citus.
Partisi dan sharding saling berkaitan tetapi berbeda. Partisi membagi tabel besar menjadi tabel fisik yang lebih kecil (partisi) pada node database yang sama. Ini meningkatkan kinerja kueri dengan pemangkasan — kueri dengan klausa WHERE pada kunci partisi hanya memindai partisi yang relevan. Sharding mendistribusikan partisi tersebut ke beberapa node database, memungkinkan scale-out horizontal.
PostgreSQL telah mendukung partisi tabel deklaratif sejak versi 10. Dua strategi yang paling berguna adalah partisi RANGE (berdasarkan tanggal — sangat baik untuk data time-series seperti log audit, event, dan invoice) dan partisi HASH (berdasarkan hash ID tenant atau ID pengguna). Untuk tabel audit_logs sistem ERP dengan jutaan baris per tahun, partisi berdasarkan bulan mengurangi waktu kueri dari detik menjadi milidetik.
PostgreSQL Scaling Decision Tree
Start Here: Is your primary DB > 70% CPU/IO?
│
No ──► Do you have read replicas?
│ No ──► Add read replicas (PRIMARY + 2 REPLICAS)
│ Yes ──► Add more read replicas or optimize queries
│
Yes
│
▼
Are your largest tables partitioned?
│
No ──► Add table partitioning (RANGE by date, HASH by tenant_id)
│ This often solves the problem without sharding
│
Yes
│
▼
Do you have a multi-tenant architecture?
│
Yes ──► Citus Schema-based sharding (1 schema per tenant)
No ──► Citus Row-based sharding (distribute by entity key)
─────────────────────────────────────────────────────────
Citus Worker Architecture:
┌─────────────────┐
│ Coordinator │ ← Application connects here
│ (PostgreSQL) │ ← Routes queries, parallelizes joins
└────────┬────────┘
│
┌──────┴──────┐
▼ ▼
Worker 1 Worker 2
Shards 0-3 Shards 4-7
(tenant A-M) (tenant N-Z)Dari membangun sistem audit trail untuk ERP Commsult: partisi tabel bervolume tinggi Anda berdasarkan created_at bulan dari hari pertama. Jalur migrasi dari tabel yang tidak dipartisi ke yang dipartisi itu menyakitkan — Anda perlu mengganti nama tabel lama, membuat induk yang dipartisi baru, menyalin data, dan melakukan swap. Melakukannya di awal ketika tabel kosong hampir tidak memerlukan biaya dan menghemat jendela pemeliharaan darurat ketika tabel mencapai 50 juta baris.
Citus memperluas PostgreSQL dengan eksekusi kueri terdistribusi di seluruh node koordinator dan node pekerja. Tabel didistribusikan melalui kolom distribusi — Citus hash nilai kolom dan menugaskan setiap shard (subset baris) ke node pekerja. Citus 12 memperkenalkan sharding berbasis skema, yang ideal untuk SaaS multi-tenant — setiap tenant mendapatkan skema mereka sendiri, dan Citus memperlakukan skema sebagai batas shard.
-- PostgreSQL Range Partitioning (by month)
CREATE TABLE audit_logs (
id UUID DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
action VARCHAR(50) NOT NULL,
resource VARCHAR(100) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
) PARTITION BY RANGE (created_at);
-- Create monthly partitions
CREATE TABLE audit_logs_2025_01
PARTITION OF audit_logs
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
CREATE TABLE audit_logs_2025_02
PARTITION OF audit_logs
FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');
-- Index each partition (auto-inherited from parent)
CREATE INDEX ON audit_logs (created_at, user_id);
-- Citus: Distribute a table across shards
SELECT create_distributed_table('orders', 'tenant_id');
SELECT create_distributed_table('invoices', 'tenant_id');
-- Reference tables (replicated to all shards for JOINs)
SELECT create_reference_table('products');
SELECT create_reference_table('currencies');Pilihan kolom distribusi menentukan kinerja kueri. Aturan emas: kueri yang memfilter pada kolom distribusi cepat; kueri yang tidak adalah scatter-gather (menyentuh semua shard). Untuk ERP multi-tenant, tenant_id adalah kolom distribusi yang jelas — semua kueri dari sesi tenant memfilter pada tenant_id mereka, sehingga lokalitas data dimaksimalkan.
Dalam pengaturan PostgreSQL yang di-shard, foreign key dan join lintas shard tidak tersedia atau sangat mahal. Anda kehilangan jaminan integritas referensial standar — Anda harus menerapkannya di lapisan aplikasi. Transaksi yang mencakup beberapa shard memerlukan protokol transaksi terdistribusi. Migrasi skema harus diterapkan ke semua shard secara bersamaan. Jangan lakukan sharding sampai Anda benar-benar menghabiskan penskalaan vertikal.
Sinyal yang menunjukkan Anda membutuhkan sharding: CPU atau I/O node database utama Anda secara konsisten di atas 70% meskipun ada optimasi kueri dan pengindeksan; tabel terbesar Anda dalam ratusan gigabyte dan tumbuh lebih cepat dari yang dapat Anda tambahkan penyimpanan; Anda memiliki arsitektur multi-tenant di mana isolasi data tenant adalah persyaratan keras.
Sebelum mempertimbangkan sharding, tambahkan read replica PostgreSQL. Sebagian besar aplikasi berat dalam membaca — 80-90% kueri adalah bacaan. Primary + 2 read replica melipattigakan kapasitas baca Anda tanpa perubahan skema. Arahkan kueri analitik, pembuatan laporan, dan endpoint API hanya-baca ke replica; arahkan penulisan ke primary.
Migrasi dari node PostgreSQL tunggal ke kluster Citus yang di-shard adalah non-trivial. Jalur yang direkomendasikan: pertama tambahkan partisi ke tabel terbesar Anda di node tunggal. Kemudian provisikan koordinator Citus + pekerja. Migrasikan tabel yang dipartisi ke tabel terdistribusi menggunakan create_distributed_table() Citus. Perbarui string koneksi aplikasi Anda untuk menunjuk ke koordinator Citus. Rencanakan 2-4 minggu pekerjaan migrasi untuk database besar.