Melampaui EXPLAIN dan index biasa: kombinasi lapisan cache in-memory (Redis) untuk mengurangi beban database, table partitioning untuk data raksasa, materialized views untuk agregasi yang mahal, hingga teknik index lanjutan seperti partial dan covering index — semua diuji lewat benchmark yang jujur, bukan perasaan

Di episode 6 kita menyelesaikan query lambat dengan index dan konfigurasi buffer; di episode 20 kita selamat dari incident. Sekarang pertanyaannya berbeda: bagaimana kalau aplikasi terus tumbuh dan database menjadi titik sempit (bottleneck) meski semua query sudah optimal?
Episode ini adalah performance engineering — melihat sistem sebagai satu kesatuan. Tidak semua beban harus dipukul database: sebagian bisa di-cache, sebagian dipisah secara fisik (partitioning), dan sebagian hasil mahal bisa disimpan (materialized views). Semua keputusan ditutup satu disiplin: benchmark yang jujur. Tanpa pengukuran, tuning hanyalah opini.
Pertanyaan pertama saat sistem melambat: apakah datanya harus diambil dari database setiap kali? Banyak beban bersifat read-heavy dengan data yang jarang berubah — katalog produk, konfigurasi, profil. Menaruh data ini di cache in-memory seperti Redis menurunkan beban database secara drastis.
Pola paling umum adalah cache-aside:
redis-cli SET product:1024 '{"name":"Meja","price":250000}' EX 300
# EX 300 = expire setelah 300 detik, mencegah data basi selamanyaKeuntungan: latensi turun dari milidetik ke mikrodetik, query database berkurang drastis untuk data panas. Kerugian: data bisa basi (stale) dan ada komponen baru yang harus dioperasikan. Tiga pitfall yang wajib dikelola:
Tip
Mulailah cache dari data yang paling jelas dan paling panas: katalog yang jarang berubah, hasil agregasi ringan, atau sesi. Jangan buru-buru cache semua tabel — cache yang salah menambah kompleksitas tanpa menurunkan beban.
Saat sebuah tabel mencapai puluhan hingga ratusan gigabyte, index b-tree raksasa melambat dan VACUUM/backup makin berat. Solusi struktural: partitioning — memecah satu tabel logis menjadi beberapa partisi fisik.
PostgreSQL mendukung declarative partitioning. Contoh paling umum: partition per bulan berdasarkan waktu.
CREATE TABLE orders (
id bigint,
created_at timestamptz,
total numeric
) PARTITION BY RANGE (created_at);
CREATE TABLE orders_2026_08 PARTITION OF orders
FOR VALUES FROM ('2026-08-01') TO ('2026-09-01');
CREATE TABLE orders_2026_09 PARTITION OF orders
FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');Poin penting yang harus kalian pahami:
SELECT ... FROM orders WHERE created_at ... tetap sama; planner memilih partisi yang relevan (partition pruning).ON CONFLICT hanya valid di dalam satu partisi — baca dokumentasi sebelum mengadopsi.Partitioning adalah keputusan arsitektur, bukan tuning — mengubah tabel besar menjadi partitioned butuh migrasi (episode 11 dan 17). Terapkan sejak desain jika jelas data akan tumbuh tak terbatas per waktu.
Dashboard dan laporan sering menghitung agregasi besar (SUM/COUNT/GROUP BY jutaan baris) setiap kali dibuka. Hasilnya jarang berubah tiap detik, tapi biaya komputasinya besar. Materialized view menyimpan hasil query sebagai tabel fisik — query mahal dihitung sekali, lalu dibaca cepat.
CREATE MATERIALIZED VIEW mv_revenue_daily AS
SELECT date_trunc('day', created_at) AS day,
sum(total) AS revenue
FROM orders
GROUP BY 1;
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_revenue_daily;CONCURRENTLY membuat refresh tidak mengunci pembacaan — wajib di produksi. Catatan penting: materialized view tidak selalu fresh; schedulkan refresh (cron atau pg_cron) sesuai kebutuhan kesegaran. Ini trade-off antara kesegaran dan performa — cocok untuk dashboard, bukan untuk transaksi real-time.
Setelah index dasar (episode 4 dan 6), tiga teknik berikut menyelesaikan kasus paling umum di produksi.
Partial index — index hanya untuk subset baris yang memang dicari. Query WHERE status = 'pending' memeriksa jutaan baris padahal hanya sebagian kecil yang pending; index untuk semua baris itu boros.
CREATE INDEX idx_orders_pending ON orders (created_at)
WHERE status = 'pending';Covering index (INCLUDE) — menyimpan kolom ekstra agar query dilayani tanpa menyentuh tabel utama (index-only scan). Berguna untuk query yang memfilter satu kolom lalu mengambil beberapa kolom lain.
CREATE INDEX idx_orders_user_inc ON orders (user_id) INCLUDE (total, status);
-- SELECT total, status FROM orders WHERE user_id = ?;BRIN untuk data yang terurut secara fisik (misal kolom waktu pada tabel append-only) — jauh lebih kecil daripada b-tree dan sangat cepat untuk range scan.
Pitfall umum: terlalu banyak index. Setiap index memperlambat INSERT/UPDATE/DELETE dan memakan storage. Pakai pg_stat_user_indexes (episode 6) untuk menemukan index yang jarang digunakan dan drop yang jelas mati.
Semua teknik di atas harus diuji dengan disiplin:
pgbench adalah tool benchmark bawaan PostgreSQL:
pgbench -i -s 100 mydb
pgbench -c 20 -j 4 -T 60 -P 5 mydb
# -c 20 koneksi, -T 60 durasi 60 detik, -P 5 laporan tiap 5 detikUntuk beban aplikasi nyata, gunakan skenario yang benar-benar dipakai aplikasi (misal k6 yang mengarah ke API). Teknik yang terlihat hebat di atas kertas harus lulus uji p95 di beban nyata — kalau tidak, itu bukan peningkatan, hanya kompleksitas baru.
Inti yang harus dibawa pulang:
CONCURRENTLY untuk produksi.Di episode 22 selanjutnya kita menaikkan level arsitektur: distributed & NewSQL — CockroachDB/YugabyteDB, Vitess, dan model konsistensi — kapan satu node tidak lagi cukup dan bagaimana mengevaluasi arsitektur terdistribusi tanpa terjebak hype. Sampai jumpa di episode 22!