Database Design Patterns
Pola-pola umum dalam merancang database yang efisien dan scalable
Normalisasi vs Denormalisasi
Normalisasi
- Menghilangkan redundansi data dengan memisahkan ke tabel berbeda
- Keuntungan: data konsisten, update di satu tempat
- Kerugian: query kompleks butuh banyak JOIN yang bisa lambat
Denormalisasi
- Sengaja duplikasi data untuk mempercepat read
- Keuntungan: query lebih cepat karena data sudah siap, tidak perlu banyak JOIN
- Kerugian: update lebih kompleks, risiko inkonsistensi
Panduan umum:
- Mulai dengan normalisasi (3NF)
- Denormalisasi di area spesifik yang terbukti menjadi bottleneck
Indeks
- Struktur data yang mempercepat pencarian dengan trade-off storage tambahan dan write yang lebih lambat
- B-Tree index: default di PostgreSQL/MySQL, bagus untuk range query dan equality
- Hash index: sangat cepat untuk equality, tidak bisa range
- Composite index: index pada beberapa kolom, urutan kolom penting
Kapan membuat indeks:
- Kolom yang sering ada di WHERE clause
- Kolom yang digunakan untuk JOIN
- Kolom yang digunakan untuk ORDER BY pada query besar
Kapan tidak membuat indeks:
- Tabel yang sangat sering ditulis (insert/update/delete) — indeks memperlambat write
- Kolom dengan cardinality sangat rendah (contoh: boolean — hanya true/false)
Sharding
- Membagi data secara horizontal ke beberapa database instance
- Setiap shard berisi subset data berdasarkan shard key
Strategi Sharding
Range-based: data dibagi berdasarkan range nilai shard key
- Contoh: user ID 1-1M di shard 1, 1M-2M di shard 2
- Mudah di-manage, tapi risiko hotspot jika distribusi tidak merata
Hash-based: data dibagi berdasarkan hash dari shard key
- Distribusi lebih merata
- Sulit untuk range query lintas shard
Directory-based: lookup table yang memetakan data ke shard
- Fleksibel, bisa redistribute tanpa ubah logic
- Lookup table menjadi single point of failure
Replication
Primary-Replica (Master-Slave)
- Primary menerima write, replica menerima read
- Async replication: write di primary tidak tunggu replica
- Semi-sync: tunggu minimal satu replica acknowledge
- Cocok untuk read-heavy workload
Multi-Primary (Multi-Master)
- Beberapa node bisa menerima write
- Conflict resolution diperlukan saat dua node update data yang sama bersamaan
- Lebih kompleks tapi lebih available untuk write
Pola untuk Time-series Data
- Data yang mengacu pada waktu (metrics, logs, events)
- Selalu insert, jarang update, sering query berdasarkan range waktu
- Solusi khusus: TimescaleDB, InfluxDB, Clickhouse
- Strategi: partition berdasarkan waktu, auto-expire data lama
Event Sourcing
- Simpan semua perubahan state sebagai sequence of events, bukan state akhir
- Contoh: daripada simpan "saldo = 500", simpan events: "deposit 1000", "withdraw 500"
- Keuntungan: audit trail lengkap, bisa replay history
- Kerugian: query state saat ini lebih kompleks, storage lebih besar
CQRS (Command Query Responsibility Segregation)
- Pisahkan model untuk membaca (query) dan menulis (command)
- Write model: dioptimasi untuk konsistensi dan integritas
- Read model: dioptimasi untuk performa query (denormalized view)
- Sering dikombinasikan dengan Event Sourcing
N+1 Query Problem
Problem umum yang sering tidak disadari:
# Ambil semua order (1 query)
orders = db.query("SELECT * FROM orders")
# Untuk setiap order, ambil user-nya (N query)
for order in orders:
user = db.query(f"SELECT * FROM users WHERE id = {order.user_id}")Total: 1 + N query. Untuk 100 order = 101 query.
Solusi: gunakan JOIN atau eager loading
SELECT orders.*, users.name
FROM orders
JOIN users ON users.id = orders.user_idPraktik Terbaik
- Pilih tipe data yang tepat — integer lebih efisien dari varchar untuk ID
- Selalu ada primary key — memudahkan replication dan indexing
- Hindari SELECT * — ambil hanya kolom yang diperlukan
- Monitor slow query log dan optimize query yang muncul
- Test dengan data volume yang mendekati production sebelum launch