Semua database pada awalnya kecil. Berapa GB aja masih muat di satu server, query jalan dalam milidetik, backup selesai sebelum sempat ngopi. Tapi data tumbuh, query makin kompleks, dan tiba-tiba storage hampir penuh, slow log penuh query yang dulunya cepet, backup molor sampe jam kantor.
Masalahnya bukan “kapan bakal gede” – itu pasti. Tapi “lo siap gak pas itu terjadi.”
Artikel ini ngomongin strategi ngontrol database growth dalam 3 fase: preventif sebelum gede, optimasi framework pas udah mulai berat, dan sharding sebagai jalan terakhir. Dengan studi kasus di MySQL, PostgreSQL, dan MongoDB – plus gimana Django dan Laravel bisa bantu dari sisi aplikasi.
Masalah: Growth Itu Silent Killer
Database yang membesar tanpa kontrol bukan cuma soal storage. Efeknya domino:
- Query makin lambat – full table scan yang dulunya 50ms jadi 5 detik karena data 100x lipat
- Maintenance window makin sempit – vacuum di PostgreSQL, index rebuild di MySQL, compaction di MongoDB semua butuh waktu yang makin panjang
- Cost membengkak – penyimpanan, backup, replication semuanya naik. AWS RDS gp3 di Jakarta $0.138/GB/bulan, backup excess $0.095/GB/bulan. Kalo data lo 500GB, itu $69/bulan cuma buat storage doang, belum IOPS dan transfer
- Restore makin riskan – restore dari backup yang butuh 6 jam? Coba pas production down
Yang paling bahaya: banyak tim baru sadar pas udah kritis. Query timeout, replication lag parah, restore gagal gara-gara storage penuh di tengah jalan.
Fase 1: Sebelum Gede – Preventif & Housekeeping
Ini fase paling murah tapi paling sering di-skip. Karena pas data masih kecil, semuanya berasa cepet. Masalahnya, kebiasaan yang dibentuk di fase ini yang nentuin seberapa lama lo bisa jalan sebelum kena tembok.
Indexing: Jangan Kurang, Jangan Juga Kebanyakan
Index itu pisau bermata dua. Bikin query SELECT cepet, tapi bikin INSERT/UPDATE/DELETE lebih lambat karena index harus diupdate tiap kali data berubah.
Prinsip dasar:
-- MySQL: cek index usage
SELECT * FROM sys.schema_index_statistics WHERE table_schema = 'your_db';
-- PostgreSQL: cek unused indexes
SELECT schemaname, tablename, indexname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0;
-- MongoDB: cek index usage
db.collection.aggregate([{ $indexStats: {} }])
Yang sering salah: over-indexing. Tim bikin index di semua kolom yang mungkin dipake WHERE, padahal 30% index gak pernah dipake. Setiap index ekstra itu tambahan write overhead dan storage.
Tips per database:
- MySQL: pake
pt-index-usagedari Percona Toolkit buat analisis index mana yang beneran dipake - PostgreSQL:
pg_stat_user_indexesnunjukin index mana yangidx_scan = 0– berarti gak pernah dipake. Drop aja - MongoDB:
$indexStatsaggregation stage ngasih tau hit count tiap index
Khusus MongoDB: shard key index. Kalo lo udah pikirin sharding dari awal, pilih shard key yang high cardinality. Shard key WAJIB di-index, dan MongoDB bakal pake index itu buat routing query. Shard key yang monotonik (kaya created_at) bakal bikin hot shard.
Data Lifecycle & Partitioning
Gak semua data perlu diakses dengan kecepatan yang sama. Bedain data panas (hot) dan data dingin (cold):
-- PostgreSQL: partitioning by range
CREATE TABLE orders (
id BIGSERIAL,
created_at TIMESTAMPTZ NOT NULL,
total DECIMAL(10,2)
) PARTITION BY RANGE (created_at);
-- Buat partition per bulan
CREATE TABLE orders_2026_06 PARTITION OF orders
FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
-- Buat partition buat data lama yang jarang diakses
CREATE TABLE orders_archive PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01')
TABLESPACE cold_storage;
# MongoDB: data lifecycle via TTL index
db.events.createIndex(
{ "created_at": 1 },
{ expireAfterSeconds: 86400 * 90 } -- auto-delete setelah 90 hari
)
PostgreSQL punya pg_partman (⭐2,744, aktif) untuk otomatisasi partition management. Tinggal set interval dan retention, dia bikin partition baru dan drop yang lama otomatis.
MySQL punya partitioning native sejak 5.1. Bisa RANGE, LIST, HASH, KEY. Tapi inget: MySQL partition key HARUS jadi bagian dari semua unique index (termasuk primary key). Ini batasan yang sering bikin orang pusing pas migrasi.
MongoDB pake sharding + TTL index untuk data lifecycle. Kalo datanya time-series, MongoDB 5.0+ punya Time Series Collections yang otomatis handle kompresi dan aging.
Query Optimization: EXPLAIN Sebelum Eksekusi
Sebelum query masuk production, biasain EXPLAIN dulu:
-- MySQL
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 123;
-- PostgreSQL
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE customer_id = 123;
// MongoDB
db.orders.find({ customer_id: 123 }).explain("executionStats")
Yang perlu dicek di output EXPLAIN:
- Full table scan (
Seq Scandi PG,type: ALLdi MySQL) – tanda gak ada index - Nested loop dengan banyak baris – butuh index atau restructure query
- Sort pada dataset gede – perlu index yang sesuai sama ORDER BY
Connection Pooling
Tiap koneksi database itu resource. PostgreSQL spawn 1 process per koneksi. MySQL spawn 1 thread. Kalo aplikasi lo punya 100 worker yang masing-masing bikin koneksi sendiri, database lo bisa kolaps duluan sebelum query-nya jalan.
Tool yang umum dipake:
# PgBouncer config (PostgreSQL)
[databases]
mydb = host=localhost port=5432 dbname=mydb
[pgbouncer]
pool_mode = transaction # transaction pooling > session pooling
default_pool_size = 25 # jumlah koneksi ke DB
max_client_conn = 200 # jumlah client yang bisa connect
# ProxySQL (MySQL)
mysql -h127.0.0.1 -P6032 -uadmin -padmin
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply)
VALUES (1, 1, '^SELECT', 1, 1);
LOAD MYSQL QUERY RULES TO RUNTIME;
Bloat Management (Database-Specific)
Database yang sering update/delete bakal ninggalin bloat – ruang yang terisi data usang:
- PostgreSQL:
VACUUMitu bukan saran, tapi keharusan. Setautovacuumdengan parameter yang agresif buat tabel yang sering update. Kalo udah terlanjur bloat, pakepg_repack(v1.5.3) buat reclaim space tanpa exclusive lock - MySQL InnoDB:
purge threadshandle bloat secara internal, tapi table dengan banyak update/delete mungkin butuhOPTIMIZE TABLEataupt-online-schema-changeuntuk rebuild - MongoDB WiredTiger: compression default snappy (~2x ratio). Kalo mau lebih agresif, ganti ke
zstddi level 6 – dapet ~3-5x ratio dengan CPU cost yang masih acceptable
MongoDB WiredTiger compression settings:
storage:
wiredTiger:
collectionConfig:
blockCompressor: zstd # default: snappy. zstd lebih agresif
engineConfig:
zstdCompressionLevel: 6 # default 6, range 1-22
Tips: Setting blockCompressor cuma ngefek ke collection baru. Buat collection existing, lo harus collMod atau rebuild pake compact.
Fase 2: Udah Gede – Optimasi Tanpa Migrasi
Di fase ini, data lo udah cukup gede (ratusan GB - beberapa TB) tapi belum waktunya sharding. Ada banyak yang bisa dioptimasi sebelum commit ke distributed architecture.
Read Replicas
Ini langkah pertama yang paling umum dan paling gampang. Pisahin read dan write:
# Django: database routing
DATABASE_ROUTERS = ['myapp.db_router.PrimaryReplicaRouter']
class PrimaryReplicaRouter:
def db_for_read(self, model, **hints):
return 'replica'
def db_for_write(self, model, **hints):
return 'primary'
// Laravel: native read/write config di config/database.php
'mysql' => [
'read' => [
'host' => ['192.168.1.2', '192.168.1.3'], // multiple replicas
],
'write' => [
'host' => '192.168.1.1',
],
'sticky' => true, // baca data yang baru ditulis dari write connection
'database' => 'myapp',
'username' => 'root',
'password' => '',
'charset' => 'utf8mb4',
],
Laravel punya fitur sticky yang penting: pas lo nulis data di request yang sama, sticky: true bikin SELECT berikutnya pake write connection (bukan replica) – ini handle replication lag yang biasanya bikin user nulis data tapi gak langsung muncul pas di-refresh.
Framework Optimization: Fitur yang Udah Siap
Banyak framework udah sadar masalah query bottleneck dari awal, jadi mereka udah include solusinya. Masalahnya, banyak developer gak tau atau lupa pake.
Django: select_related vs prefetch_related
Ini perbedaan fundamental yang nentuin jumlah query ke database:
# ❌ N+1 query problem
books = Book.objects.all()
for book in books:
print(book.author.name) # 1 query per book = N+1 queries total
# ✅ select_related: JOIN di SQL (buat ForeignKey/OneToOneField)
books = Book.objects.select_related('author').all()
for book in books:
print(book.author.name) # 1 query aja
# ✅ prefetch_related: separate query (buat ManyToMany/reverse FK)
pizzas = Pizza.objects.prefetch_related('toppings').all()
for pizza in pizzas:
print(pizza.toppings.all()) # 2 queries total: 1 pizza + 1 toppings
Kapan pake mana:
select_related– buat ForeignKey dan OneToOneField. Dia bikin SQL JOIN, jadi 1 query. Cepet buat single-valued relationshipprefetch_related– buat ManyToManyField dan reverse ForeignKey. Dia bikin 2 queries (1 buat main table, 1 buat related table), lalu join di Python. Lebih efisien daripada JOIN yang bisa explode result set
Efek ke database: Dengan select_related, query dari puluhan jadi 1. Beban database turun drastis, terutama pas data udah gede.
# Django: ambil kolom tertentu aja, gak perlu semuanya
users = User.objects.only('id', 'username', 'email')
# Django: defer kolom berat (kaya JSONField, TextField)
products = Product.objects.defer('description', 'specifications_json')
Laravel: Eager Loading vs Lazy Loading
Eloquent ORM juga punya masalah N+1 yang sama:
// ❌ N+1
$books = Book::all();
foreach ($books as $book) {
echo $book->author->name; // query per book
}
// ✅ Eager loading
$books = Book::with('author', 'reviews')->get();
Laravel chunk() vs cursor() – buat handle dataset gede:
// chunk(): proses per batch, memory stabil
Book::chunk(200, function ($books) {
foreach ($books as $book) {
// process 200 buku per batch
// memory cuma nampung 200 model
}
});
// cursor(): pake generator, 1 query doang
foreach (Book::cursor() as $book) {
// memory cuma 1 model... tapi PDO buffer semua hasil query!
// kalo 1 juta baris, PDO tetep nampung semua di buffer
}
// lazy(): rekomendasi buat dataset sangat gede
foreach (Book::lazy(200) as $book) {
// chunk-based, tapi stream kayak generator
// bisa eager loading
}
Kapan pake mana:
chunk()– proses data batch dengan memory stabil. Cocok buat update data. PakechunkById()kalo lo update kolom yang dipake buat filteringcursor()– cuma 1 query, tapi PDO internal buffer semua hasil. Cocok buat read-only dengan dataset gak terlalu gede (ribuan, bukan jutaan)lazy()– yang paling aman buat dataset besar. Kombinasi chunking + streaming. Bisa eager loading
Caching Layer: Redis vs Memcached
Cache layer di depan database bisa ngurangin beban query sampe 80% untuk read-heavy workload.
| Use Case | Pilihan | Kenapa |
|---|---|---|
| Simple query cache (key-value) | Memcached | Lower overhead, simpler, lebih cepet |
| Session storage | Redis | Butuh persistence + data structures |
| Rate limiting, queues, pub/sub | Redis | Fitur bawaan Redis |
| Cache must survive restart | Redis | Persistence (RDB/AOF) |
| High-throughput write cache | Memcached | Lebih ringan CPU overhead |
Pattern yang umum:
# Django: Redis cache backend
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
}
}
# Cache query hasil berat
def get_popular_products():
cache_key = 'popular_products'
products = cache.get(cache_key)
if not products:
products = Product.objects.filter(
views__gte=1000
).select_related('category').order_by('-views')[:50]
cache.set(cache_key, products, 300) # 5 menit
return products
// Laravel: cache query
$products = Cache::remember('popular_products', 300, function () {
return Product::with('category')
->where('views', '>=', 1000)
->orderBy('views', 'desc')
->take(50)
->get();
});
Vertical Scaling: Kapan Berhenti
Naikin instance (more CPU, more RAM, faster storage) itu opsi termudah – tapi ada batasnya.
Tanda udah waktunya stop vertical scaling:
- Instance terbesar di provider masih gak cukup (r6g.16xlarge di AWS = 64 vCPU, 512GB RAM)
- Biaya instance gede lebih mahal daripada solusi distributed
- Masalahnya bukan di resource, tapi di arsitektur (query lambat karena full scan, bukan karena CPU kurang)
Perbandingan di Jakarta (ap-southeast-3):
| Instance | vCPU | RAM | $/bulan (730 jam) |
|---|---|---|---|
| db.r6g.large | 2 | 16 GiB | $186 |
| db.r6g.xlarge | 4 | 32 GiB | $372 |
| db.r6g.2xlarge | 8 | 64 GiB | $744 |
| db.r6g.4xlarge | 16 | 128 GiB | $1,488 |
Di titik tertentu, $1,488/bulan buat 1 instance mulai gak masuk akal dibanding $500-700/bulan buat cluster 3 node yang scalable.
Materialized Views & Summary Tables
Buat query agregat yang berat (reporting, dashboard), jangan query data mentah tiap kali:
-- PostgreSQL: Materialized View
CREATE MATERIALIZED VIEW daily_sales_summary AS
SELECT
DATE(created_at) AS sale_date,
COUNT(*) AS total_orders,
SUM(total) AS revenue,
COUNT(DISTINCT customer_id) AS unique_customers
FROM orders
GROUP BY DATE(created_at)
WITH DATA;
-- Refresh periodik (bisa di-cron)
REFRESH MATERIALIZED VIEW CONCURRENTLY daily_sales_summary;
# Django: summary table via management command
class Command(BaseCommand):
def handle(self, *args, **options):
today = date.today()
summary = OrderSummary.objects.update_or_create(
date=today,
defaults={
'total_orders': Order.objects.filter(
created_at__date=today
).count(),
'revenue': Order.objects.filter(
created_at__date=today
).aggregate(Sum('total'))['total__sum'],
}
)
Read-Write Separation via Framework
Ini penting: jangan semua query ke primary. Pisahin yang read-only ke replica.
Django: DATABASE_ROUTERS udah support read/write split native. Tinggal define router class dan set db_for_read() / db_for_write().
Laravel: Config read / write host di config/database.php udah enough. Plus sticky: true buat handle consistency.
Tips replication: PostgreSQL punya Streaming Replication (physical) buat HA + read scaling, dan Logical Replication buat selective data distribution antar versi/platform. Pilih sesuai kebutuhan:
- Streaming Replication – whole cluster replication, exact byte-level copy. Buat HA dan read-only replicas
- Logical Replication – row-level, bisa milih subset tabel. Buat consolidate data ke analytical DB, atau migrasi antar versi PostgreSQL
Fase 3: Udah Besar Banget – Sharding
Sharding adalah jalan terakhir. Bukan pertama. Kalo lo udah ngejalanin Fase 1 dan 2 dengan bener, lo bisa nunda sharding sampe data lo di level beberapa puluh TB.
Tanda lo beneran butuh sharding:
- Write throughput – satu primary gak sanggup nanganin volume write
- Dataset – di atas beberapa TB dan partitioning aja gak cukup
- Working set – index/data yang sering diakses gak muat di memory
- Operasional – maintenance (vacuum, reindex) udah terlalu lama buat satu instance
MySQL Sharding Options
1. Application-Level Sharding (Paling Umum)
Lo tentuin shard key di kode aplikasi dan routing query berdasarkan nilai key:
# Django: manual shard routing
def get_shard(user_id):
shard_id = user_id % 4 # 4 shards
return f'shard_{shard_id}'
class ShardRouter:
def db_for_read(self, model, **hints):
if hasattr(model, '_shard_key'):
return get_shard(model._shard_key)
return 'default'
Kelemahan: Lo harus manage koneksi ke tiap shard, logic routing ada di kode, migration jadi ribet.
2. ProxySQL (v3.0.9)
ProxySQL bukan sharding engine – dia proxy yang bisa routing query based on rules. Lo define rules: “SELECT dari user_id 1-1000 -> hostgroup 1, 1001-2000 -> hostgroup 2”.
INSERT INTO mysql_query_rules
(rule_id, active, match_pattern, destination_hostgroup, apply)
VALUES
(1, 1, '^SELECT.*WHERE user_id BETWEEN 1 AND 1000', 1, 1),
(2, 1, '^SELECT.*WHERE user_id BETWEEN 1001 AND 2000', 2, 1);
3. Vitess (v24.0.2, CNCF Graduated)
Vitess adalah database clustering system untuk horizontal scaling MySQL. Awalnya dari YouTube, sekarang CNCF project yang mature. Dia handle: sharding, connection pooling, query routing, failover, dan backup.
Yang bikin Vitess beda:
- Transparent sharding – aplikasi gak perlu tau soal shard key
- VSchema (Vitess Schema) – define sharding strategy di config, bukan di kode
- Kubernetes-native – jalan di k8s, auto manage shard splitting (resharding)
- Support transactions dalam shard, cross-shard pake 2PC
Kelebihan: Aplikasi gak perlu diubah. Koneksi ke Vitess seperti ke MySQL biasa. Kekurangan: Complexity operasional tinggi. Butuh tim yang dedicated.
PostgreSQL Sharding Options
1. Citus (v14.0.0, by Microsoft)
Citus adalah PostgreSQL extension yang ubah Postgres jadi distributed database. Data di-shard secara otomatis ke worker nodes. Query planner Citus routing query ke node yang tepat.
-- Pilih distribution column
SELECT create_distributed_table('orders', 'customer_id');
-- Query otomatis di-routing ke shard yang tepat
SELECT * FROM orders WHERE customer_id = 123;
-- Hanya query 1 shard
-- Query agregat cross-shard
SELECT customer_id, COUNT(*) FROM orders GROUP BY customer_id;
-- Query semua shard, hasil di-merge di coordinator
Citus cocok buat:
- Multi-tenant SaaS (distribute per tenant)
- Real-time analytics (columnar storage)
- Event logging (time-series + customer_id)
Gak cocok buat:
- OLTP dengan banyak cross-shard transactions
- Query yang butuh data dari banyak shard tanpa distribution key
Catatan penting: Citus diakuisisi oleh Microsoft tahun 2019, bukan oleh Timescale. Citus dan TimescaleDB (untuk time-series) adalah produk terpisah.
2. postgres_fdw Sharding
PostgreSQL punya Foreign Data Wrappers (FDW) yang bisa konek ke Postgres lain. Dengan postgres_fdw + declarative partitioning, lo bisa bikin sharding manual:
-- Buat foreign server buat tiap shard
CREATE SERVER shard1 FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'db1.example.com', dbname 'mydb', port '5432');
-- Buat foreign table
CREATE FOREIGN TABLE orders_shard1 ()
INHERITS (orders) SERVER shard1;
-- Partition table pake inheritance
CREATE TABLE orders_global () INHERITS (orders);
CREATE RULE orders_to_shard1 AS ON INSERT TO orders_global
WHERE customer_id BETWEEN 1 AND 1000
DO INSTEAD INSERT INTO orders_shard1 VALUES (NEW.*);
Kelebihan: Pure PostgreSQL, gak butuh ekstensi tambahan (selain postgres_fdw yang built-in). Transparan ke aplikasi.
Kekurangan: Manual banget. Lo manage routing sendiri. Cross-shard query lambat. Gak ada automatic rebalancing.
MongoDB Sharding (Native)
MongoDB punya sharding bawaan – ini salah satu keunggulan utamanya dibanding MySQL/PostgreSQL yang sharding-nya butuh middleware atau eksternal tool.
Komponen:
mongos– query router, entry point buat aplikasiconfig servers– replica set yang nyimpen metadata shardingshards– masing-masing replica set
// Enable sharding di database
sh.enableSharding("mydb");
// Pilih shard key (HATI-HATI, gak bisa diubah!)
sh.shardCollection("mydb.orders", { customer_id: "hashed" });
// Cek status balancing
sh.status()
Pilih shard key yang bener:
| Shard Key | Kelebihan | Kekurangan |
|---|---|---|
hashed (customer_id: "hashed") | Distribusi merata, gak ada hot shard | Range query gak efisien |
ranged (created_at: 1) | Range query cepet | Monotonik = hot shard |
compound ({region: 1, _id: 1}) | Balanced distribution + useful for queries | Butuh planning lebih |
Praktik terbaik shard key:
- High cardinality – banyak nilai unik
- Hashed sharding buat write-heavy workload
- Hindari monotonik key (kaya timestamp doang) – pake hashed atau compound
- Shard key harus index – MongoDB otomatis bikin index di shard key
Chunk balancing:
- Default chunk size: 128 MB
- Balancer aktif otomatis kalo selisih chunk antar shard >= 3x chunkSize
- Bisa jadwalin balancing window:
sh.setBalancerWindow({ start: "02:00", stop: "06:00" })
Framework Support buat Sharding
Django: DATABASE_ROUTERS support multi-database, tapi ini buat vertical partitioning (beda model beda DB), bukan horizontal sharding (satu tabel terbelah). Buat true horizontal sharding, lo butuh custom logic.
Package django-sharding dulu ada, tapi udah deprecated sejak 2020 – cuma support Python 2.7/3.4-3.6. Jangan dipake.
Alternatif: pake Vitess (MySQL) atau Citus (PostgreSQL) yang transparan ke Django – aplikasi gak perlu tau data di-shard.
Laravel: Support multiple database connections di config/database.php. Tapi sama kayak Django, ini buat vertical partitioning, bukan horizontal sharding. Buat sharding, butuh manual routing di model/repository.
Trade-off Sharding: Yang Sering Dilupain
Sharding itu nyelesain masalah throughput, tapi bikin masalah baru:
- Cross-shard transaction – di MongoDB support sejak v4.2 tapi ada performance hit, di Citus harus pake
coordinator, di Vitess pake 2PC yang lambat - Query capability loss – gak semua query bisa jalan di cluster sharded. ORDER BY + LIMIT jadi complicated. JOIN cross-shard mahal
- Operational complexity – deploy, backup, monitoring, failover semuanya jadi lebih kompleks. Butuh tim DevOps dedicated
- Re-sharding – kalo shard key salah pilih, migrasi data itu proyek besar. MongoDB shard key gak bisa diubah setelah collection di-shard
- Lag balancing – pas balancer mindahin chunk, ada window di mana data di 2 shard. Query bisa inconsistent
Sebelum sharding, tanya dulu:
- Udah maksimal caching? Redis/Memcached bisa ngurangin 80% beban read
- Udah maksimal replicas? Beberapa read replica bisa handle traffic gede
- Udah partitioning? Partitioning (satu DB, banyak table) bisa handle dataset gede tanpa kompleksitas sharding
- Udah vertical scaling? Mungkin cukup upgrade instance 1-2 level
Kesimpulan
Growth database itu masalah “kapan”, bukan “kalau”. Tapi bukan berarti lo harus panik setiap kali storage usage naik 10%.
3 fase yang bisa lo terapin:
- Fase 1 (Preventif) – indexing bener, data lifecycle, query optimization, connection pooling. Paling murah, paling efektif
- Fase 2 (Optimasi) – read replicas, caching (Redis/Memcached), materialized views, fitur framework (Django select_related, Laravel eager loading). Bisa nahan sambe beberapa TB tanpa migrasi arsitektur
- Fase 3 (Sharding) – jalan terakhir. Vitess/ProxySQL buat MySQL, Citus buat PostgreSQL, native sharding buat MongoDB. Tapi pastiin lo udah maksimalin Fase 1 dan 2 dulu
Framework juga udah siap. Django punya select_related/prefetch_related + database routers. Laravel punya eager loading + native read/write split. Rails, Go ent, SQLAlchemy – semuanya punya mekanisme serupa. Masalahnya bukan framework-nya, tapi tau cara pake fiturnya.
Yang paling penting: preventif itu selalu lebih murah daripada reaktif. Dan sharding adalah jalan terakhir, bukan pertama.
Terima kasih sudah meluangkan waktu buat baca artikel ini, semoga ada manfaat yang bisa diambil. 🐾
