Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.
01Apa perbedaan database partitioning dan sharding?
Partitioning memecah tabel menjadi potongan kecil di dalam satu database server, dan engine database mengarahkan baris ke potongan yang tepat. Sharding memecah data ke beberapa server, dan aplikasi atau routing layer menentukan server mana yang memiliki sebuah baris. Partitioning memperbaiki ukuran scan dan retention, sedangkan sharding menambah kapasitas.
02Apakah sharding sama dengan horizontal partitioning?
Sharding adalah bentuk horizontal partitioning, artinya baris yang berbeda ditaruh di tempat berbeda. Bedanya, horizontal partitioning biasanya tetap di satu instance database, sedangkan sharding menerapkan pemisahan yang sama ke banyak instance. Vertical partitioning berbeda lagi: ia memisahkan kolom, bukan baris.
Partition saat tabel sangat besar dan sebagian besar query memfilter satu key, biasanya tanggal. Dokumentasi Postgres memberi rule of thumb bahwa ukuran tabel sebaiknya melebihi physical memory server. Keuntungan praktis terbesarnya adalah retention murah: detach atau drop partition lama menghindari DELETE bulk yang lambat beserta beban VACUUM-nya.
04Apakah partitioning PostgreSQL otomatis membuat query lebih cepat?
Hanya query yang memfilter partition key yang diuntungkan, karena planner bisa mem-prune partition yang terbukti tidak cocok. Query yang memfilter kolom lain akan memeriksa semua partition, seperti terlihat pada EXPLAIN tabel yang dipartisi per tanggal tetapi difilter per cabang. Terlalu banyak partition juga bisa memperlama planning.
05Kapan sharding tidak terhindarkan?
Sharding tidak terhindarkan ketika volume write atau ukuran data melebihi kemampuan server tunggal terbesar yang bisa Anda jalankan, bahkan setelah index, read replica, caching dan partitioning. Sharding juga bisa wajib untuk data residency, saat data suatu wilayah harus tetap di server wilayah itu. Sebagai gantinya, siapkan diri untuk query lintas shard, hot shard dan pekerjaan resharding.
Partitioning memecah satu tabel di dalam satu database server, sharding menyebar data ke beberapa server. DDL Postgres, output EXPLAIN dan shard router.
Partitioning memecah satu tabel besar menjadi potongan kecil di dalam satu database server, sehingga query memindai lebih sedikit dan data lama bisa dihapus murah. Sharding menyebar data ke beberapa server, masing-masing memegang bagian yang ditentukan shard key. Partition dulu untuk retention dan ukuran scan; shard hanya jika satu mesin tidak sanggup menampung write atau datanya.
Dua istilah ini sering dipakai bergantian di design review, dan kekeliruannya mahal karena keduanya menyelesaikan masalah yang berbeda. Tabel Postgres yang dipartisi tetap satu database di satu server. Sistem yang di-shard terdiri dari beberapa database, dan aplikasi Anda tiba-tiba harus tahu database mana yang menyimpan sebuah baris.
Post ini adalah perbandingan konsepnya. Untuk sisi praktis Citus dan rollout, lihat post yang sudah ada, PostgreSQL Sharding Strategy; di sini saya fokus pada perbedaannya, dengan DDL Postgres yang saya jalankan sendiri, shard router TypeScript, dan checklist apa yang dicoba sebelum keduanya. Fakta tentang Postgres berasal dari dokumentasi resmi partitioning-nya.
Apa perbedaan partitioning dan sharding?
Keduanya membagi dataset berdasarkan baris. Bedanya ada pada tempat potongan itu berada. Wikipedia mendeskripsikan shard sebagai horizontal partition dari data di sebuah database, dan mencatat bahwa horizontal partitioning biasanya terjadi dalam satu instance database, sedangkan sharding menerapkan pemisahan baris yang sama ke banyak instance.
Pertanyaan
Partitioning (declarative Postgres)
Sharding
Di mana potongan data berada?
Sebagai child table di satu database pada satu server
Di database terpisah, biasanya pada server terpisah
Siapa yang menentukan potongan mana menyimpan sebuah baris?
Engine database, berdasarkan batas partition
Routing layer atau aplikasi Anda, berdasarkan shard key
Bagaimana transaksi dan join?
Transaksi dan join single-node biasa, lintas partition
Transaksi dan join lintas shard sulit atau tidak tersedia
Masalah apa yang diselesaikan?
Ukuran scan, ukuran index, retention murah, bulk load dan delete
Beban write atau ukuran data yang tidak sanggup dipikul satu server
Apakah menambah kapasitas hardware?
Tidak, tetap satu mesin
Ya, tiap shard membawa CPU, memory dan disk sendiri
Seberapa mahal mengubahnya nanti?
Attach atau detach partition, tanpa mengubah aplikasi
Resharding memindahkan data dan biasanya menyentuh aplikasi
Sisa post ini mengikuti baris-baris tabel tersebut. Jika sebuah baris tidak menggambarkan masalah Anda, kemungkinan besar Anda belum butuh teknik di sisi itu.
Bagaimana table partitioning bekerja di PostgreSQL?
Declarative partitioning di Postgres menawarkan tiga metode: range partitioning berdasarkan rentang key yang tidak tumpang tindih, list partitioning berdasarkan nilai key yang disebutkan eksplisit, dan hash partitioning berdasarkan modulus dan remainder. Data time-series seperti order atau log hampir selalu cocok dengan range. DDL berikut dimodelkan dari order log sebuah POS carwash, dan saya menjalankannya di PostgreSQL 16.15 lokal.
-- Range partitioning by month on a table shaped like a carwash POS order log.
-- The primary key MUST include the partition key (created_at), or Postgres refuses it.
CREATE TABLE wash_orders (
id bigint GENERATED ALWAYS AS IDENTITY,
branch_id int NOT NULL,
created_at timestamptz NOT NULL,
total_idr integer NOT NULL,
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);
-- The upper bound is exclusive, so adjacent months never overlap.
CREATE TABLE wash_orders_2026_09 PARTITION OF wash_orders
FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');
CREATE TABLE wash_orders_2026_10 PARTITION OF wash_orders
FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');
-- Created on the parent, it becomes one index per partition automatically.
CREATE INDEX ON wash_orders (branch_id, created_at);
-- Synthetic data: 40,001 rows, one every 2 minutes starting 2026-09-01.
INSERT INTO wash_orders (branch_id, created_at, total_idr)
SELECT 1 + (g % 5),
timestamptz '2026-09-01' + (g * interval '2 minutes'),
50000 + (g % 7) * 10000
FROM generate_series(0, 40000) AS g;
ANALYZE wash_orders;
Partition pruning adalah hasil utamanya. Saat pruning aktif, yang merupakan default lewat setting enable_partition_pruning, planner membuktikan bahwa sebuah partition tidak mungkin berisi baris yang cocok dengan klausa WHERE, lalu membuangnya dari plan. Di bawah ini output asli dari run saya. Query pertama memfilter partition key dan hanya menyentuh partition Oktober. Query kedua hanya memfilter branch_id, sehingga tidak ada yang bisa di-prune dan kedua partition diperiksa.
-- Filter on the partition key: only October is scanned.
EXPLAIN (COSTS OFF)
SELECT count(*) FROM wash_orders
WHERE created_at >= '2026-10-01' AND created_at < '2026-10-08';
Aggregate
-> Seq Scan on wash_orders_2026_10 wash_orders
Filter: ((created_at >= '2026-10-01 00:00:00+07'::timestamp with time zone) AND (created_at < '2026-10-08 00:00:00+07'::timestamp with time zone))
-- Filter on something else: nothing can be pruned, both partitions are probed.
EXPLAIN (COSTS OFF)
SELECT count(*) FROM wash_orders WHERE branch_id = 3;
Aggregate
-> Append
-> Bitmap Heap Scan on wash_orders_2026_09 wash_orders_1
Recheck Cond: (branch_id = 3)
-> Bitmap Index Scan on wash_orders_2026_09_branch_id_created_at_idx
Index Cond: (branch_id = 3)
-> Bitmap Heap Scan on wash_orders_2026_10 wash_orders_2
Recheck Cond: (branch_id = 3)
-> Bitmap Index Scan on wash_orders_2026_10_branch_id_created_at_idx
Index Cond: (branch_id = 3)
Dua catatan jujur soal run ini: datanya sintetis dan kecil, jadi saya hanya menunjukkan bentuk plan, bukan timing, dan planner memilih sequential scan untuk Oktober hanya karena 18.401 baris terlalu sedikit agar index menang. Angka +07 pada plan adalah time zone mesin saya. Pelajaran yang berlaku umum ada pada query kedua: tabel yang dipartisi hanya membantu query yang memfilter partition key.
Kenapa drop partition lebih murah daripada delete baris?
Retention adalah alasan terkuat untuk mempartisi. Dokumentasi Postgres menyatakan bahwa menghapus partition dengan DROP TABLE atau ALTER TABLE DETACH PARTITION jauh lebih cepat daripada operasi bulk, dan menghindari beban VACUUM yang ditinggalkan oleh DELETE bulk. Untuk tabel yang menyimpan, misalnya, 13 bulan order, pembersihan bulanan menjadi perubahan metadata, bukan delete panjang.
-- Wrong: a bulk DELETE touches every row, bloats the table and leaves VACUUM work behind.
DELETE FROM wash_orders WHERE created_at < '2026-10-01';
-- Right: detach the old month (CONCURRENTLY avoids the heavy lock on the parent),
-- archive it if you must, then drop it. No per-row work happens at all.
ALTER TABLE wash_orders DETACH PARTITION wash_orders_2026_09 CONCURRENTLY;
-- pg_dump -t wash_orders_2026_09 ... (optional archive step)
DROP TABLE wash_orders_2026_09;
Dokumentasi yang sama mencatat bahwa DROP TABLE biasa membutuhkan lock ACCESS EXCLUSIVE pada parent, dan menawarkan DETACH PARTITION, dengan bentuk CONCURRENTLY, sebagai opsi yang sering lebih baik karena data tetap tersedia sebagai tabel biasa untuk di-backup dulu. Dokumentasi juga memberi rule of thumb kapan ini sepadan: ukuran tabel sebaiknya melebihi physical memory server database.
Jebakan primary key. Unique atau primary key pada tabel yang dipartisi harus menyertakan semua kolom partition key, jadi key pada id saja ditolak dan Anda berakhir dengan primary key (id, created_at). Artinya database tidak bisa menjamin keunikan id lintas bulan. Jika jaminan itu perlu, tegakkan dengan cara lain, misalnya identity column atau UUID yang tidak pernah dipakai ulang oleh aplikasi Anda.
Constraint unique dan primary key harus menyertakan semua kolom partition key, dan partition key tidak boleh memakai expression atau pemanggilan function.
Tidak ada exclusion constraint untuk seluruh tabel yang dipartisi, hanya pada tiap leaf partition.
Planning tetap cepat hingga beberapa ribu partition, tetapi hanya jika query umum memungkinkan planner mem-prune hampir semuanya. Partition harian selama sepuluh tahun berarti sekitar 3.650 partition, yang sudah di ujung panduan itu.
Insert baris yang tidak cocok dengan partition mana pun menghasilkan error, jadi harus ada proses yang membuat partition bulan depan sebelum bulannya mulai.
Kenapa horizontal dan vertical partitioning membingungkan?
Ada tiga kosakata yang saling tumpang tindih, dan hasil pencarian mencampurnya dengan bebas. Pegang definisi berikut dan sebagian besar perdebatan hilang.
Horizontal partitioning menaruh baris yang berbeda di tabel yang berbeda. Range partitioning Postgres dan sharding sama-sama horizontal; bedanya ada di server, bukan di bentuk pemisahannya.
Vertical partitioning menaruh kolom yang berbeda di tabel yang berbeda, misalnya memindahkan kolom deskripsi atau blob yang jarang dibaca keluar dari tabel yang sering diakses. Ini juga disebut row splitting, dan tidak ada hubungannya dengan sharding.
Sebagian sistem memakai kata shard untuk apa yang di artikel Wikipedia disebut partition: MongoDB, Elasticsearch dan SolrCloud, sementara Postgres memakai partition untuk child table di satu server. Tanyakan server mana yang memiliki potongan itu sebelum berasumsi.
Tes yang berguna saat membaca diagram arsitektur: jika satu mesin mati, apakah hanya sebagian tabel yang hilang? Jika ya, itu sharding. Jika seluruh tabel mati bersama, itu partitioning, apa pun label kotaknya.
Bagaimana sharding bekerja, dan apa biayanya?
Sharding membutuhkan shard key, yaitu kolom yang nilainya menentukan shard pemilik, dan routing layer yang menerapkannya. Sketsa di bawah mengasumsikan tiap shard punya schema sama dengan kolom tenant_id, seperti ERP multi-tenant di mana satu perusahaan pelanggan adalah satu tenant. Ia meng-hash tenant id dengan sha256 lalu mengambil sisa baginya.
import { createHash } from "node:crypto";
import { Pool } from "pg";
// One Pool per shard. In production these point at different servers.
const SHARDS: Pool[] = [
new Pool({ connectionString: process.env.SHARD_0_URL }),
new Pool({ connectionString: process.env.SHARD_1_URL }),
new Pool({ connectionString: process.env.SHARD_2_URL }),
new Pool({ connectionString: process.env.SHARD_3_URL }),
];
// Do not use Math.random() or JS string hashCode: the result must be stable
// across processes and deploys, or a tenant's rows are written to one shard
// and looked up on another.
export function shardFor(tenantId: string, shardCount = SHARDS.length): number {
const digest = createHash("sha256").update(tenantId).digest();
return digest.readUInt32BE(0) % shardCount;
}
export function poolFor(tenantId: string): Pool {
return SHARDS[shardFor(tenantId)];
}
// Single-tenant query: one hop, one shard.
await poolFor(tenantId).query(
"SELECT * FROM wash_orders WHERE tenant_id = $1 AND created_at >= $2",
[tenantId, from],
);
// Cross-shard query: the application, not the database, must fan out and merge.
const parts = await Promise.all(
SHARDS.map((p) => p.query("SELECT count(*) AS n FROM wash_orders")),
);
const total = parts.reduce((sum, r) => sum + Number(r.rows[0].n), 0);
Router adalah bagian yang mudah. Bagian yang sulit adalah mengubah jumlah shard. Dengan hash modulo N, menambah shard kelima ke empat shard mengubah jawaban untuk sebagian besar key. Perhitungan dan pengecekan terhadap 100.000 tenant id yang dibangkitkan ada di bawah.
// Growing from 4 shards to 5 with hash % N. A key keeps its shard only when
// h % 4 == h % 5. Over any 20 consecutive hash values that is h = 0,1,2,3:
// stay = 4 / 20 = 20% move = 16 / 20 = 80%
//
// Checked on 100,000 ids "tenant-0" .. "tenant-99999" with the sha256 router above:
// kept their shard: 19,962 (19.96%) moved: 80,038 (80.04%)
// A consistent-hash ring or a lookup table moves only about 1/N of the keys.
Karena itu router modulo biasa adalah titik awal, bukan desain. Post Consistent Hashing Explained membahas ring yang membatasi perpindahan menjadi sekitar satu dari N key, dan lookup table yang memetakan tenant ke shard memberi kebebasan yang sama dengan biaya satu hal lagi yang harus dijaga konsisten. Dokumentasi Citus menjelaskan ide yang sama dari sisi database, dengan distribution column dan co-located table agar baris terkait mendarat di node yang sama.
Pilih shard key agar query paling umum membawanya. Di sistem multi-tenant itu tenant_id: request untuk satu tenant menuju satu shard, join antar tabel tenant itu tetap lokal, dan tabel kecil bersama seperti daftar harga disalin ke setiap shard, yang oleh Citus disebut reference table.
Hot shard: jika satu tenant atau satu key jauh lebih besar dari yang lain, shard-nya jenuh sementara yang lain menganggur, dan hashing tidak bisa memperbaiki satu key yang terlalu besar.
Query lintas shard: laporan lintas semua tenant harus fan out ke setiap shard dan menggabungkan hasil di kode aplikasi, seperti Promise.all di atas.
Transaksi lintas shard: tidak ada satu commit lintas server, jadi Anda menghindarinya atau menanggung two-phase commit atau saga.
Operasional: perubahan schema, backup dan failover kini berjalan per shard, dan setiap shard butuh replica sendiri agar aman.
Apa yang sebaiknya dicoba sebelum partitioning atau sharding?
Kebanyakan tabel lambat bukan masalah partitioning. Kerjakan daftar ini berurutan, karena tiap langkah lebih murah dan lebih mudah dibatalkan daripada berikutnya.
Gejala
Coba dulu
Kenapa lebih dulu daripada partitioning atau sharding
Satu query lambat
Baca EXPLAIN ANALYZE, tambah atau perbaiki index
Index yang hilang terlihat persis seperti tabel yang terlalu besar
Read membebani primary
Tambah read replica atau cache
Menyebar read tanpa memecah data, meski replica bisa lag
CPU, RAM atau disk habis
Pindah ke mesin lebih besar
Mesin lebih besar itu sekadar deploy, bukan redesign
Baris lama mendominasi tabel
Arsipkan, atau range-partition berdasarkan tanggal
Retention adalah kemenangan termurah dari partitioning
Write melebihi yang sanggup ditahan satu server
Shard, setelah langkah di atas habis
Hanya server tambahan yang menambah kapasitas write
Di setup single VPS yang biasa saya deploy, satu box Postgres dengan Redis di depannya dan replica untuk backup bisa sangat jauh. Untuk tabel seperti order log, langkah pertama yang saya pilih adalah partitioning per bulan, bukan sharding.
Kapan harus partition, dan kapan sharding tidak terhindarkan?
Pakai checklist ini sebagai aturan keputusan. Jawab berurutan dan berhenti pada ya pertama.
Apakah query lambat karena index hilang atau plan buruk? Perbaiki index dulu.
Apakah tabel besar dan berurutan waktu, dengan query yang memfilter tanggal dan data lama yang ingin dibuang? Range-partition pada tanggal itu.
Apakah tabel besar tetapi setiap query sudah memfilter satu key seperti tenant, dan satu partition masih muat di satu server? Hash atau list partitioning bisa membantu maintenance, tetap di satu mesin.
Apakah working set atau laju write lebih besar dari server terbesar yang wajar Anda jalankan, bahkan setelah index, replica dan partitioning? Sharding kini tidak terhindarkan.
Apakah Anda butuh data residency, dengan data suatu wilayah dikunci di server wilayah itu? Itu bisa membenarkan sharding per wilayah bahkan pada ukuran sedang.
Kedua teknik ini juga bisa digabung. Setiap shard bisa mempartisi tabelnya sendiri per tanggal, begitulah sistem multi-tenant besar menjaga retention tetap murah di dalam tiap shard. Urutannya penting: partition dulu karena bisa dibatalkan, dan shard hanya saat batas yang terukur memaksa Anda.
Aturan yang dibawa pulang: partitioning adalah keputusan tata letak tabel di dalam satu server, sharding adalah keputusan arsitektur sistem lintas server. Pilih partitioning saat ukuran scan atau retention terasa sakit, habiskan index, replica dan mesin lebih besar berikutnya, dan shard hanya ketika satu mesin tidak sanggup menahan write. Memilih shard key dan rencana resharding adalah pekerjaan sebenarnya, dan tidak ada query planner yang akan mengerjakannya untuk Anda.