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-usage dari Percona Toolkit buat analisis index mana yang beneran dipake
  • PostgreSQL: pg_stat_user_indexes nunjukin index mana yang idx_scan = 0 – berarti gak pernah dipake. Drop aja
  • MongoDB: $indexStats aggregation 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 Scan di PG, type: ALL di 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: VACUUM itu bukan saran, tapi keharusan. Set autovacuum dengan parameter yang agresif buat tabel yang sering update. Kalo udah terlanjur bloat, pake pg_repack (v1.5.3) buat reclaim space tanpa exclusive lock
  • MySQL InnoDB: purge threads handle bloat secara internal, tapi table dengan banyak update/delete mungkin butuh OPTIMIZE TABLE atau pt-online-schema-change untuk rebuild
  • MongoDB WiredTiger: compression default snappy (~2x ratio). Kalo mau lebih agresif, ganti ke zstd di 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 relationship
  • prefetch_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. Pake chunkById() kalo lo update kolom yang dipake buat filtering
  • cursor() – 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 CasePilihanKenapa
Simple query cache (key-value)MemcachedLower overhead, simpler, lebih cepet
Session storageRedisButuh persistence + data structures
Rate limiting, queues, pub/subRedisFitur bawaan Redis
Cache must survive restartRedisPersistence (RDB/AOF)
High-throughput write cacheMemcachedLebih 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:

  1. Instance terbesar di provider masih gak cukup (r6g.16xlarge di AWS = 64 vCPU, 512GB RAM)
  2. Biaya instance gede lebih mahal daripada solusi distributed
  3. Masalahnya bukan di resource, tapi di arsitektur (query lambat karena full scan, bukan karena CPU kurang)

Perbandingan di Jakarta (ap-southeast-3):

InstancevCPURAM$/bulan (730 jam)
db.r6g.large216 GiB$186
db.r6g.xlarge432 GiB$372
db.r6g.2xlarge864 GiB$744
db.r6g.4xlarge16128 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:

  1. Write throughput – satu primary gak sanggup nanganin volume write
  2. Dataset – di atas beberapa TB dan partitioning aja gak cukup
  3. Working set – index/data yang sering diakses gak muat di memory
  4. 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 aplikasi
  • config servers – replica set yang nyimpen metadata sharding
  • shards – 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 KeyKelebihanKekurangan
hashed (customer_id: "hashed")Distribusi merata, gak ada hot shardRange query gak efisien
ranged (created_at: 1)Range query cepetMonotonik = hot shard
compound ({region: 1, _id: 1})Balanced distribution + useful for queriesButuh planning lebih

Praktik terbaik shard key:

  1. High cardinality – banyak nilai unik
  2. Hashed sharding buat write-heavy workload
  3. Hindari monotonik key (kaya timestamp doang) – pake hashed atau compound
  4. 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:

  1. Cross-shard transaction – di MongoDB support sejak v4.2 tapi ada performance hit, di Citus harus pake coordinator, di Vitess pake 2PC yang lambat
  2. Query capability loss – gak semua query bisa jalan di cluster sharded. ORDER BY + LIMIT jadi complicated. JOIN cross-shard mahal
  3. Operational complexity – deploy, backup, monitoring, failover semuanya jadi lebih kompleks. Butuh tim DevOps dedicated
  4. Re-sharding – kalo shard key salah pilih, migrasi data itu proyek besar. MongoDB shard key gak bisa diubah setelah collection di-shard
  5. 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:

  1. Fase 1 (Preventif) – indexing bener, data lifecycle, query optimization, connection pooling. Paling murah, paling efektif
  2. 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
  3. 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. 🐾