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 #

KategoriBentuk & pola akses khasContoh produk
OLTP relasional (baris)transaksi singkat, tulis-ubah kecil, banyak point lookupPostgreSQL, MySQL
OLAP kolomscan kolom besar, agregasi masif, batch load berkalaDuckDB, ClickHouse
Key-valuepasangan kunci-nilai sederhana, latensi sangat rendah, in-memoryRedis
Documentdokumen semi-terstruktur (JSON/BSON), schema fleksibelMongoDB
Search engineinverted index, pencarian teks lengkap, pengurutan relevansiElasticsearch, OpenSearch
Time-seriestitik data bertanda waktu, tulis terus-menerus, retensi otomatisInfluxDB

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.

python
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:

text
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 ms

Mari kita telaah baris-baris kode di atas:

  1. Baris 7-13 (dua query uji): AGREGAT memaksa mesin membaca seluruh kolom produk_id, jumlah, dan harga dari satu juta baris; LOOKUP hanya menyentuh satu baris lewat index primary key.
  2. Baris 16-24 (ukur_tercepat): pengukuran diulang tiga kali lalu diambil yang tercepat, meminimalkan pengaruh gangguan sistem operasi.
  3. Baris 32-46 (data SQLite): recursive CTE baris(i) membangkitkan angka 0 sampai 999999, lalu formula modulo memetakan setiap angka menjadi produk_id, jumlah, dan harga sehingga isinya deterministik.
  4. Baris 48-58 (data DuckDB): formula yang sama dijalankan lewat range(1000000), jadi kedua tabel berisi nilai identik — dibuktikan baris hasil 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 dominanKategoriKandidat awal
Transaksi multi-tabel dengan ACID (pesanan, pembayaran)OLTP relasionalPostgreSQL, MySQL
Analitik agregat pada jutaan-miliar baris, cluster terpisahOLAP kolomClickHouse
Analitik ringan pada berkas lokal, tanpa serverOLAP kolom embeddedDuckDB
Cache hasil query / counter / session dengan latensi mikrodetikKey-valueRedis
Katalog produk semi-terstruktur yang schema-nya sering berubahDocumentMongoDB
Pencarian teks lengkap ulasan dengan pengurutan relevansiSearch engineElasticsearch, OpenSearch
Metric dan sensor yang menulis terus-menerus dengan retensi bergulirTime-seriesInfluxDB

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.