Catatan dari produksi
DatabasesArchitectureNoSQL

Engine database: kapan pakai yang mana

Setiap aplikasi pada akhirnya menyimpan data, dan pertanyaan pertamanya hampir selalu sama: pakai database yang mana? Jawaban jujurnya bukan "yang tercepat" atau "yang paling populer" — tapi yang membuat pertanyaan paling sering Anda ajukan jadi satu pencarian yang murah.

Bayangkan sebuah gudang. Anda bisa menatanya dua cara. Cara pertama: masukkan semua yang dibutuhkan satu pelanggan ke dalam satu kotak berlabel — pesanan, alamat, pembayaran — lalu taruh kotaknya utuh di rak. Untuk melayani pelanggan itu, ambil satu kotak. Cara kedua: simpan rak terpisah untuk pesanan, pelanggan, dan produk, lalu telusuri benang di antaranya untuk merakit jawaban.

Hampir semua perdebatan "SQL vs NoSQL" sebenarnya soal itu: seberapa banyak data yang Anda kelompokkan jadi satu kotak, dan seberapa sering pertanyaan Anda harus menyeberang antar kotak. Mari telusuri enam keluarga engine — alur baca/tulis tiap engine, kapan memakainya, dan kasus produksi nyatanya.

your most-asked query?scan + aggregatemillions of rows?yesnoCOLUMNAROLAP · dashboards · BIwalk relationshipsmany hops?yesnoGRAPHfraud · social · recsa write firehoseby entity + time?yesnoWIDE-COLUMNmessages · metrics · feedsget / put by key,value opaque?yesnoKEY-VALUEcache · session · cartquery fields ina JSON document?yesnoDOCUMENTprofile · CMS · orderRELATIONALjoins + ACID · the default
Satu alur, enam keluarga: turuni pertanyaannya dan 'yes' pertama memilih engine Anda — kalau jatuh sampai dasar, relational adalah default yang masuk akal.

Key-value — loket penitipan

Anda serahkan jaket, dapat nomor. Nanti tunjukkan nomornya, jaket persisnya kembali. Loketnya tak tahu dan tak peduli isi kantong jaket Anda — sangat cepat, mudah diskalakan, dan tak berguna kalau Anda harus bertanya "jaket mana yang biru?".

#42valuekey#42CLOAKROOMticketget(#42) → coatkey-valueexact get by #ticketcan't ask "which are blue?"
Loket penitipan: serahkan mantel, dapat tiket bernomor; tiket itu mengambil mantel yang persis sama — tapi Anda tak bisa tanya 'mantel mana yang biru?'.
Clientget/put(key)hash(key)→ tokenlanding node + N replicas
Client meng-hash key ke satu node + N replika. Tanpa master — node mana pun bisa jadi koordinator.

Kapan dipakai: cache, session, shopping cart, preferensi pengguna — apa pun yang diambil berdasarkan key. Netflix membangun EVCache (cache in-memory di atas memcached) sebagai lapisan cache-nya: datanya ephemeral, punya TTL, dan bisa di-evict kapan saja. Amazon membangun Dynamo justru karena pola relational buruk untuk layanan high-availability seperti shopping cart dan session — yang hanya pernah diambil berdasarkan key — dan Dynamo sengaja menukar konsistensi kuat demi ketersediaan.

Document — map berlabel

Tiap pelanggan punya map manila berisi formulir terstruktur — nama, daftar pesanan, item bersarang. Anda buka map mana pun lewat labelnya, lalu baca atau ubah field tertentu tanpa membongkar semuanya.

usersordersdocumentnameAda Lovelaceemail[email protected]orders[2]#1042#1043users/1023
Map berlabel: buka map mana pun lewat labelnya dan baca atau ubah field tertentu di dalamnya — termasuk daftar order yang bersarang.
ClientqueryRouter(mongos)Shard A · P+S+SShard B · P+S+S
Router mengarahkan tiap dokumen ke satu shard lewat shard key; tulis ke primary, oplog mereplikasi ke secondary.

Beda dari key-value, di sini tiap field bisa di-index dan di-query, dan Anda bisa memperbarui sebagian dokumen. Batasnya: skala horizontal menuntut Anda memilih shard key di depan, dan modelnya tersiksa saat data Anda sebenarnya sangat terhubung.

Kapan dipakai: record berbentuk pohon yang mandiri — produk, halaman CMS, profil, satu pesanan.

Wide-column — lemari arsip baris jarang

Bayangkan satu spreadsheet raksasa; tiap baris dikunci oleh sesuatu yang selalu Anda cari — pengguna, channel, sensor — dan tiap baris bisa punya jutaan kolom, tapi cuma mengisi yang dibutuhkan. Anda tak pernah bertanya "tunjukkan semua baris yang kolom X-nya biru" ke seluruh lembar; Anda minta "kolom untuk baris ini, terurut". Itu harganya; imbalannya, lembar ini bisa dibelah ke ribuan mesin dan menyerap firehose tulisan tanpa berkedip.

wide-columnwrites land in sparse cellsrow keyc1c2c3c4c5c6×millionsuser:1chan:7sensor:3look up by row key · each row is wildly wide but mostly empty
Lemari arsip baris jarang: cari lewat row key; tiap baris sangat lebar tapi hanya mengisi beberapa sel, dan lemarinya terbagi ke banyak mesin.
WRITEwritelog + memtableSSTablecompactREADreadmemtable + bloom→SSTablesmerge → row
Tulis di-append ke commit log + memtable (RAM) lalu di-flush ke SSTable immutable; baca menggabung memtable + SSTables lewat bloom filter.

Jalur tulisnya berbasis LSM-tree: tulisan masuk ke memtable (RAM, terurut) + commit log (append), langsung di-ack tanpa read-before-write, lalu di-flush ke SSTable immutable yang berkala di-compact. Itu sebabnya engine ini write-optimized. Penghapusan menulis tombstone, jadi beban hapus-berat dan partisi panas perlahan memburuk.

Discord memindahkan penyimpanan pesannya tiga kali: MongoDB → Cassandra (2017) → ScyllaDB (2022).

Discord — How Discord Stores Trillions of Messages

Modelnya adalah desain query-driven yang jadi buku teks: pesan dipartisi berdasarkan channel_id + bucket waktu, terurut lewat Snowflake ID, dengan replication factor 3 — partition key-nya adalah pola aksesnya. Discord meninggalkan Cassandra karena partisi panas, jeda garbage collection JVM, dan beban compaction; ScyllaDB (tulisan ulang Cassandra dalam C++, tanpa garbage collector, arsitektur shard-per-core) memangkas cluster dari 177 node jadi 72 node dan menurunkan p99 baca dari 40–125 ms jadi 15 ms. Perhatikan: mereka tak mengganti model, hanya engine — masalah terbesarnya ada di runtime.

Kapan dipakai: firehose tulisan yang dikunci entitas + waktu (pesan, event, metrik, feed).

Columnar (OLAP) — kolom yang dipisah

Bayangkan semua kolom spreadsheet dicabik jadi daftar terpisah — semua "harga" di satu tempat, semua "negara" di tempat lain. Buruk untuk "ambil satu baris utuh", tapi luar biasa untuk "rata-rata harga 2 miliar penjualan, dikelompokkan per negara".

columnarcountryqtyUSDEJP371price12.509.9920.00SUM(price)= 42.49reads + aggregates only the column it needs
Kolom yang dipisah: tiap kolom disimpan sendiri, jadi query hanya mengangkat kolom yang diperlukan — di sini mengangkat 'price' lalu mengagregasinya (SUM = 42.49).
Query:country, AVG(price)columns stored separatelycountrypriceaggregate(MPP)reads only 2 of 6 columns — skips the rest
Query hanya membaca kolom yang dirujuk, lalu mengagregasinya secara vektor lintas node (MPP).

Tiap kolom disimpan dan dikompresi terpisah (nilai sejenis berdampingan menekan ukuran 10×+). Query menyentuh hanya kolom yang dirujuk, dalam loop vektor, dibagi lintas banyak node (MPP). Pas untuk SUM/GROUP BY atas miliaran baris; salah untuk "ambil satu baris utuh".

Kapan dipakai: scan + agregasi jutaan baris — dashboard, BI, analisis ad-hoc yang query-nya belum Anda ketahui di depan.

Sales · OLAP2.4B rowsrevenue$4.2M+12%AVG(price)by countryrevenue / dayGROUP BY country
Engine columnar memberi tenaga dashboard analitik — mengagregasi miliaran baris (di sini GROUP BY country, AVG price) jadi beberapa grafik cepat.

Graph — peta relasi

Alih-alih kotak mandiri, Anda menyimpan benang antar benda sebagai objek kelas satu. "Alice → teman → Bob → kerja-di → Acme" disimpan sebagai tautan nyata yang bisa ditelusuri satu langkah sekaligus. Pertanyaan seperti "teman-dari-teman yang bekerja di tempat teman saya bekerja" — yang membuat aggregate store kewalahan — justru inti kekuatan graph.

friendfriendfriendfriendworks-atAliceBobCarolDaveAcmegraphpathAlice → Bob → Carol = 2 hopswalk the links, one hop at a time
Peta relasi: orang dan tautan jadi warga kelas satu, jadi 'teman-dari-teman' cukup ditelusuri satu hop demi satu hop.
Cypherqueryindex:start nodeABCD
Index menemukan node awal sekali; setelah itu tiap langkah adalah pointer tersimpan — O(1) per hop.

Node dan relasi adalah warga kelas satu yang tersimpan fisik (index-free adjacency), jadi menelusuri satu relasi adalah lompatan pointer, bukan join. Biayanya sebanding dengan subgraph yang disentuh, bukan ukuran total dataset — dasar teknis klaim bahwa saat jumlah "hop" bertambah, aggregate store melambat sementara graph tetap datar.

Relational — tabel bertaut dengan aturan

Tabel ternormalisasi yang Anda gabungkan dengan JOIN, dijaga aturan integritas, lalu di-commit secara transaksional. Fleksibel untuk relasi sedang, kuat soal kebenaran.

Customersidname1Ada2BenProductsidtitle1Pen2MugOrdersidcustomer_idproduct_id101211211N1NJOINACIDrelational
Tabel bertaut dengan aturan: tabel ternormalisasi yang disambung lewat key, dijaga benar oleh aturan integritas dan commit semua-atau-tidak (ACID).
READSQLplannerindexJOINWRITEINSERTWALcommit · atomic (ACID)
Optimizer menyusun plan; index + JOIN merakit baris; WAL membuat commit durable + atomic (ACID).

Kapan dipakai: join + agregasi lintas entitas yang harus tetap benar. Dan ini bukan pilihan kuno — Uber sengaja tidak membangun NoSQL. Docstore/Schemaless-nya dibangun di atas MySQL, membungkus tiap operasi dalam transaksi MySQL yang direplikasi lewat Raft demi jaminan ACID. Mereka mengevaluasi Cassandra dan menolaknya — konsistensi eventual-nya menghambat produktivitas developer — lalu memilih desain CP (konsistensi di atas ketersediaan). Pelajarannya: "NoSQL di skala besar" bukan otomatis.

Time-series — titik di garis waktu

Monitor detak jantung, ticker saham, grafik CPU server: bentuk data yang sama — sebuah nilai berstempel waktu, di-append selamanya, dibaca ulang sebagai rentang dan rangkuman. Mayoritas append, berurut waktu, dan Anda nyaris tak pernah menulis ulang masa lalu.

time-seriesappend-only · time-orderednew append↞ rolls off (retention)t-6time →nowread back downsampled — AVG / MAX per window
Time-series: titik berstempel waktu mengalir masuk dan ter-append di kanan; titik lama berguguran (retensi); dibaca ulang ter-downsample — AVG/MAX per window.

Twist-nya: time-series bukan model penyimpanan ketujuh — ia spesialisasi dari keluarga di atas untuk satu workload ini. Di baliknya:

  • TimescaleDB itu ekstensi PostgreSQL — relational plus kompresi columnar dan partisi-waktu otomatis.
  • InfluxDB 3 menyimpan Apache Arrow / Parquet, jadi columnar; InfluxDB lama memakai engine TSM (LSM-tree), mirip wide-column.
  • Prometheus punya store lokal gaya LSM sendiri, dan pola klasik partisi by series + time persis si firehose wide-column.

Jadi pakai TSDB khusus kalau Anda mau ergonomi waktu langsung jadi — kebijakan retensi, downsampling, gap-filling, fungsi time-bucket. Pakai wide-column atau columnar langsung kalau lebih suka memiliki modelnya dan sudah menjalankan salah satunya.

Butuh OLTP + OLAP? Jalankan keduanya

Kebanyakan perusahaan butuh dua-duanya, dan memaksa satu alat melakukan keduanya adalah kesalahan klasik. Polanya standar:

Lakukan

Jadikan store OLTP (key-value / document / wide-column / relational) sumber kebenaran, lalu alirkan datanya ke salinan columnar untuk analitik — lewat CDC atau ekspor batch. Append sebuah change log, lalu hitung "versi terbaru" saat query.

Hindari

Jangan menjalankan query analitik "GROUP BY lintas semuanya" langsung ke wide-column yang dikunci channel_id. Itu memaksa scan seluruh cluster dan bisa menjatuhkan node yang sedang melayani trafik live. Jangan pula memaksa columnar memperbarui baris satu per satu — ia membencinya.

Aturan satu kalimat: kalau yang bertanya manusia atau dashboard, arahkan ke columnar; kalau aplikasi Anda bertanya "ambil satu ini" pada skala besar, arahkan ke OLTP. Kalau dua-duanya benar, jalankan dua-duanya dan alirkan salah satu ke yang lain.

Sources

  1. Martin Fowler — Aggregate-Oriented Database
  2. Werner Vogels — Amazon's Dynamo
  3. Netflix Tech Blog — Ephemeral Volatile Caching (EVCache)
  4. Discord — How Discord Stores Trillions of Messages
  5. Uber — Schemaless / Docstore (built on MySQL)
  6. Sarah Mei — Why You Should Never Use MongoDB