Deep dive database performance: query profiling dengan EXPLAIN ANALYZE, indexing strategies (B-tree, hash, partial), connection pooling optimization, database tuning parameters, dan scaling strategies (read replicas, sharding).

Setelah di episode 23 kita memahami AI workload performance, kini saatnya membahas area yang menjadi akar masalah performa paling sering: database performance. Sebagian besar bottleneck performance testing muncul di database — query yang lambat, index yang tidak ada, connection pool yang habis, atau locking yang berlebihan. Database performance engineering adalah skill yang membedakan performance engineer biasa dari yang benar-benar efektif.
Episode ini membawa kalian deep dive ke database profiling, indexing strategies, dan tuning parameters yang secara langsung mempengaruhi performa aplikasi.
EXPLAIN ANALYZE adalah tool paling powerful untuk memahami bagaimana database mengeksekusi query:
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT o.*, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'pending'
AND o.created_at > '2026-01-01'
ORDER BY o.created_at DESC
LIMIT 100;| Node Type | Arti |
|---|---|
| Seq Scan | Full table scan — lambat untuk tabel besar |
| Index Scan | Menggunakan index — cepat |
| Bitmap Index Scan | Multiple index lookups |
| Hash Join | Join menggunakan hash — cepat untuk large datasets |
| Nested Loop | Join menggunakan loop — lambat untuk large datasets |
Seq Scan on orders (cost=0..1000 rows=500000)
→ Tidak ada index untuk WHERE status = 'pending'
→ Buat index: CREATE INDEX idx_orders_status ON orders(status)
Sort (cost=5000..5200 rows=100)
→ Sorting tidak menggunakan index
→ Buat composite index yang include ORDER BY columnB-tree adalah index default untuk大多数 database — cocok untuk equality checks dan range queries.
-- B-tree index untuk equality dan range
CREATE INDEX idx_orders_user_created
ON orders (user_id, created_at DESC);
-- Query yang menggunakan index ini
SELECT * FROM orders WHERE user_id = 123 AND created_at > '2026-01-01';Hash index sangat cepat untuk equality checks tetapi tidak mendukung range queries:
-- Hash index untuk equality saja
CREATE INDEX idx_users_email_hash
ON users USING hash (email);Partial index mengindex hanya subset baris — mengurangi ukuran index dan meningkatkan performa:
-- Index hanya untuk orders pending (sedikit dari total)
CREATE INDEX idx_orders_pending
ON orders (created_at DESC)
WHERE status = 'pending';Covering index mencakup semua kolom yang dibutuhkan query — mengeliminasi kebutuhan untuk mengakses tabel utama:
-- Covering index: query tidak perlu akses tabel
CREATE INDEX idx_orders_covering
ON orders (user_id, created_at DESC)
INCLUDE (status, total);
-- Query ini dijawab sepenuhnya dari index
SELECT status, total FROM orders
WHERE user_id = 123
ORDER BY created_at DESC;| Anti-Pattern | Masalah | Solusi |
|---|---|---|
| Too many indexes | Write performance turun | Hapus index yang tidak dipakai |
| Wrong column order | Index tidak efektif | Kolom dengan selectivity tinggi duluan |
| No composite index | Banyak single-column indexes | Composite index untuk query patterns |
| Unused indexes | Overhead tanpa manfaat | Cek pg_stat_user_indexes |
Pool Size ≈ (CPU cores × 2) + effective_spindle_count
Untuk SSD: (CPU cores × 2) + 1
Contoh: 8 cores → pool size ≈ 17-- PostgreSQL: monitor connection usage
SELECT
state,
count(*),
max(now() - state_change) as max_duration
FROM pg_stat_activity
GROUP BY state;
-- Connection pool wait time
SELECT
datname,
numbackends,
numbackends * 100.0 / (SELECT setting::int FROM pg_settings WHERE name = 'max_connections') as pct_used
FROM pg_stat_database;PgBouncer adalah connection pooler yang mengurangi overhead koneksi PostgreSQL:
# pgbouncer.ini
[databases]
mydb = host=localhost port=5432 dbname=mydb
[pgbouncer]
pool_mode = transaction # Pool per transaction, bukan per session
max_client_conn = 1000
default_pool_size = 20-- Memory
shared_buffers = '4GB' -- 25% of RAM
effective_cache_size = '12GB' -- 75% of RAM
work_mem = '256MB' -- Per-operation memory
-- Query Planning
random_page_cost = 1.1 -- SSD (default 4.0 untuk HDD)
effective_io_concurrency = 200 -- SSD
-- WAL
wal_buffers = '64MB'
checkpoint_completion_target = 0.9# Reload configuration tanpa restart
SELECT pg_reload_conf();
-- Cek parameter saat ini
SHOW shared_buffers;
SHOW effective_cache_size;Read replicas menambah throughput untuk read-heavy workloads:
-- Arahkan read queries ke replica
-- Application routing
SELECT * FROM products WHERE id = 123; -- ke replica
UPDATE products SET price = 29.99 WHERE id = 123; -- ke primarySharding membagi database menjadi banyak shards berdasarkan shard key:
-- Shard by user_id
-- Shard 0: user_id % 4 == 0
-- Shard 1: user_id % 4 == 1
-- Shard 2: user_id % 4 == 2
-- Shard 3: user_id % 4 == 3# PgBouncer pool per shard
shard_0: host=pg-0 port=5432 dbname=mydb
shard_1: host=pg-1 port=5432 dbname=mydb
shard_2: host=pg-2 port=5432 dbname=mydb
shard_3: host=pg-3 port=5432 dbname=mydbDi episode 24 ini kalian telah memahami database performance engineering:
Di episode 25 selanjutnya, kita akan membahas Performance Architecture Review — bagaimana melakukan review arsitektur dari perspektif performa dan scalability. Siapkan architecture diagram kalian!