Sepanjang section ini kita memperlakukan database relasional sebagai satu-satunya rumah bagi data — dan untuk pesanan, pelanggan, serta stok toko online, pilihan tersebut memang tepat. Tetapi data engineer di dunia nyata berhadapan dengan beragam beban: scan miliaran baris event untuk analitik, cache yang dituntut merespons dalam mikrodetik, log sensor yang menulis terus-menerus, hingga pencarian teks lengkap atas jutaan ulasan produk. Tidak ada satu sistem yang unggul untuk semuanya; yang ada adalah sistem yang paling cocok untuk pola akses tertentu. Bab penutup ini merangkum peta pilihan, pertanyaan seleksi, dan menguji sendiri perbedaan perilaku dua kategori lewat SQLite dan DuckDB.
Pertanyaan Kunci Sebelum Memilih #
Keputusan pemilihan menjadi mudah bila dijawab dengan data, bukan tren. Lima pertanyaan berikut memotret kebutuhan secara jujur:
- Bagaimana pola akses utamanya — ambil satu baris berdasarkan kunci, pindai seluruh kolom untuk agregasi, atau cari teks bebas?
- Berapa latensi yang dituntut pengguna — mikrodetik untuk cache, milidetik untuk API, atau menit untuk laporan batch masih bisa diterima?
- Seberapa besar datanya sekarang dan berapa laju pertumbuhannya per bulan?
- Apakah data membutuhkan transaksi ACID lintas tabel, atau utuh per baris sudah cukup?
- Bagaimana bentuk datanya — relasional ternormalisasi, dokumen semi-terstruktur, atau deret waktu bertanda waktu?
Jawaban atas pertanyaan-pertanyaan tersebut langsung mengarah ke satu kategori di peta berikut.
Peta Kategori Sistem Penyimpanan #
| Kategori | Bentuk & pola akses khas | Contoh produk |
|---|---|---|
| OLTP relasional (baris) | transaksi singkat, tulis-ubah kecil, banyak point lookup | PostgreSQL, MySQL |
| OLAP kolom | scan kolom besar, agregasi masif, batch load berkala | DuckDB, ClickHouse |
| Key-value | pasangan kunci-nilai sederhana, latensi sangat rendah, in-memory | Redis |
| Document | dokumen semi-terstruktur (JSON/BSON), schema fleksibel | MongoDB |
| Search engine | inverted index, pencarian teks lengkap, pengurutan relevansi | Elasticsearch, OpenSearch |
| Time-series | titik data bertanda waktu, tulis terus-menerus, retensi otomatis | InfluxDB |
OLTP menyimpan data per baris karena transaksi menyentuh sedikit baris sekaligus. OLAP menyimpan data per kolom karena analitik jarang butuh semua kolom — SUM(omzet) cukup membaca kolom omzet, dan format kolom memungkinkan kompresi efektif untuk nilai yang seragam. Kategori lain menyempitkan lebih jauh: Redis menukar kekayaan query dengan kecepatan memori, sementara Elasticsearch membangun inverted index (peta dari kata ke dokumen) yang membuat pencarian frasa cepat. Dokumentasi DuckDB
dan ClickHouse
memperlihatkan dua wujud kategori OLAP: yang satu embedded di dalam proses analitik, yang satu lagi cluster terdistribusi untuk skala besar.
Uji Nyata: Penyimpanan Baris vs Kolom #
Teori kolom versus baris bisa kita buktikan dalam beberapa menit. Skrip berikut membangun satu juta baris penjualan (100 produk) di dua mesin dengan isi persis sama, lalu mengukur dua beban klasik: agregasi seluruh tabel (pola OLAP) dan pengambilan satu baris lewat primary key (pola OLTP). Setiap pengukuran diulang tiga kali dan yang tercepat dilaporkan.
import sqlite3
import time
from pathlib import Path
import duckdb
AGREGAT = """
SELECT produk_id, SUM(jumlah * harga) AS omzet
FROM penjualan
GROUP BY produk_id
ORDER BY produk_id
"""
LOOKUP = "SELECT * FROM penjualan WHERE id = 987654"
def ukur_tercepat(jalankan, ulang=3):
tercepat = None
for _ in range(ulang):
awal = time.perf_counter()
hasil = jalankan()
durasi = time.perf_counter() - awal
if tercepat is None or durasi < tercepat:
tercepat = durasi
return hasil, tercepat
# --- dua database berisi data yang persis sama: 1 juta baris penjualan ---
for nama in ("penjualan_sqlite.db", "penjualan_duckdb.db"):
if Path(nama).exists():
Path(nama).unlink()
sq = sqlite3.connect("penjualan_sqlite.db")
sq.execute("""
CREATE TABLE penjualan (
id INTEGER PRIMARY KEY, produk_id INT, jumlah INT, harga INT
)
""")
sq.execute("""
WITH RECURSIVE baris(i) AS (
SELECT 0 UNION ALL SELECT i + 1 FROM baris WHERE i < 999999
)
INSERT INTO penjualan
SELECT i, (i % 100) + 1, (i % 10) + 1, 75000 + (i % 176) * 1000
FROM baris
""")
sq.commit()
dk = duckdb.connect("penjualan_duckdb.db")
dk.execute("""
CREATE TABLE penjualan (
id INTEGER PRIMARY KEY, produk_id INTEGER, jumlah INTEGER, harga INTEGER
)
""")
dk.execute("""
INSERT INTO penjualan
SELECT i, (i % 100) + 1, (i % 10) + 1, 75000 + (i % 176) * 1000
FROM range(1000000) AS t(i)
""")
# --- uji 1: agregat atas seluruh tabel (pola OLAP) ---
hasil_sq, lama_sq = ukur_tercepat(lambda: sq.execute(AGREGAT).fetchall())
hasil_dk, lama_dk = ukur_tercepat(lambda: dk.execute(AGREGAT).fetchall())
print("hasil agregat identik:", hasil_sq == hasil_dk)
print(f"agregat 1 juta baris -> SQLite: {lama_sq * 1000:.1f} ms"
f" | DuckDB: {lama_dk * 1000:.1f} ms")
# --- uji 2: ambil satu baris lewat primary key (pola OLTP) ---
_, lama_sq = ukur_tercepat(lambda: sq.execute(LOOKUP).fetchall())
_, lama_dk = ukur_tercepat(lambda: dk.execute(LOOKUP).fetchall())
print(f"point lookup 1 baris -> SQLite: {lama_sq * 1000:.2f} ms"
f" | DuckDB: {lama_dk * 1000:.2f} ms")
sq.close()
dk.close()Output yang Dihasilkan:
hasil agregat identik: True
agregat 1 juta baris -> SQLite: 326.4 ms | DuckDB: 6.3 ms
point lookup 1 baris -> SQLite: 0.01 ms | DuckDB: 0.26 msMari kita telaah baris-baris kode di atas:
- Baris 7-13 (dua query uji):
AGREGATmemaksa mesin membaca seluruh kolomproduk_id,jumlah, danhargadari satu juta baris;LOOKUPhanya menyentuh satu baris lewat index primary key. - Baris 16-24 (
ukur_tercepat): pengukuran diulang tiga kali lalu diambil yang tercepat, meminimalkan pengaruh gangguan sistem operasi. - Baris 32-46 (data SQLite): recursive CTE
baris(i)membangkitkan angka 0 sampai 999999, lalu formula modulo memetakan setiap angka menjadiproduk_id,jumlah, danhargasehingga isinya deterministik. - Baris 48-58 (data DuckDB): formula yang sama dijalankan lewat
range(1000000), jadi kedua tabel berisi nilai identik — dibuktikan barishasil agregat identik: True.
Hasilnya memperlihatkan dua keunggulan yang berdiri di sisi berlawanan. DuckDB menyelesaikan agregasi sekitar 50 kali lebih cepat (6,3 ms berbanding 326,4 ms) karena format kolomnya hanya membaca tiga kolom yang dibutuhkan. Sebaliknya, SQLite memenangkan point lookup puluhan kali lipat (0,01 ms berbanding 0,26 ms) karena struktur baris plus B-tree dirancang untuk lompat tepat ke satu kunci. Angka pastinya bervariasi antar mesin, tetapi polanya konsisten: mesin yang hebat untuk analitik bukan mesin yang hebat untuk transaksi — dan sebaliknya.
Decision Table: Dari Kebutuhan ke Pilihan #
Rangkuman praktis berikut memetakan kebutuhan dominan langsung ke kategori dan produk yang layak dievaluasi pertama:
| Kebutuhan dominan | Kategori | Kandidat awal |
|---|---|---|
| Transaksi multi-tabel dengan ACID (pesanan, pembayaran) | OLTP relasional | PostgreSQL, MySQL |
| Analitik agregat pada jutaan-miliar baris, cluster terpisah | OLAP kolom | ClickHouse |
| Analitik ringan pada berkas lokal, tanpa server | OLAP kolom embedded | DuckDB |
| Cache hasil query / counter / session dengan latensi mikrodetik | Key-value | Redis |
| Katalog produk semi-terstruktur yang schema-nya sering berubah | Document | MongoDB |
| Pencarian teks lengkap ulasan dengan pengurutan relevansi | Search engine | Elasticsearch, OpenSearch |
| Metric dan sensor yang menulis terus-menerus dengan retensi bergulir | Time-series | InfluxDB |
Dengan peta ini, section Database dan Penyimpanan Data ditutup: dari pemodelan dan normalisasi, indexing, transaksi dan isolasi, konkurensi, pooling, backup, replikasi, hingga pemilihan sistem — satu rangkaian bekal untuk menyimpan data dengan benar. Perjalanan berikutnya berpindah ke media perjalanan data itu sendiri: Section Format dan Serialisasi Data membahas bagaimana data direpresentasikan sebagai berkas CSV, JSON, dan Parquet, serta kapan masing-masing layak dipilih.