Recraftory

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_id

Praktik 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