Seluruh rantai section ini bermuara pada satu momen: pukul delapan pagi ratusan pengguna membuka dashboard bersamaan, dan setiap grafik menuntut jawaban dalam hitungan ratus milidetik. Serving data adalah keputusan tentang kapan pekerjaan dikerjakan — saat pipeline berjalan atau saat permintaan tiba — serta dalam bentuk apa jawaban disimpan. Chapter penutup ini mengukur dua strategi penyajian secara langsung di atas dua juta pesanan TokoKita, lalu memetakan format dan jalur konsumsi untuk setiap jenis konsumen. Alat ukurnya tetap DuckDB — engine yang dipakai sepanjang section — ditambah format Parquet untuk penyimpanan hasilnya.

Kerjakan Sekali, Baca Berkali-kali #

Ada dua cara menjawab pertanyaan “berapa GMV per bulan”. Jalur pertama mengagregasi tabel fact mentah setiap kali dashboard dibuka; jalur kedua mengagregasi sekali di pipeline — pekerjaan model dbt dari Chapter Model Staging, Intermediate, dan Marts — lalu dashboard hanya membaca hasilnya. Prinsip jalur kedua sekeluarga dengan Chapter Caching pada section Optimasi dan Performa: hasil komputasi yang mahal disimpan lalu dibaca berulang. Bedanya, pre-agregasi mengubah grain tabel menjadi harian per kota per status, bukan sekadar menyalin hasil query.

Skrip pengukuran menyiapkan fact berisi dua juta pesanan (bagian pembangkit data dipotong dari tampilan), mengekspornya ke Parquet, lalu membangun mart di atasnya. Cuplikan pertama adalah inti kedua strategi tersebut:

python
# --- marts: pre-agregasi harian per kota per status (pekerjaan dbt) ---
con.execute(f"""
COPY (
    SELECT tanggal, kota, status,
           SUM(nilai) AS gmv, COUNT(*) AS jumlah_pesanan
    FROM fact_pesanan
    GROUP BY tanggal, kota, status
) TO '{MART}' (FORMAT parquet)
""")

GMV_PER_BULAN_RAW = f"""
    SELECT strftime('%Y-%m', tanggal) AS bulan, SUM(nilai) AS gmv
    FROM read_parquet('{FAKTOR}')
    WHERE status = 'selesai'
    GROUP BY bulan ORDER BY bulan
"""
GMV_PER_BULAN_MART = f"""
    SELECT strftime('%Y-%m', tanggal) AS bulan, SUM(gmv) AS gmv
    FROM read_parquet('{MART}')
    WHERE status = 'selesai'
    GROUP BY bulan ORDER BY bulan
"""

Kedua query konsumen mengembalikan bentuk yang sama — GMV per bulan — tetapi beban kerjanya berbeda: jalur raw menjumlahkan dua juta baris fact dengan filter status, sedangkan jalur mart menjumlahkan gmv yang sudah teragregasi per tanggal, kota, dan status. Pengukurannya memakai dua fungsi berikut:

python
def ukur(sql, permintaan=10):
    """Latensi per permintaan dashboard (ms), satu query per permintaan."""
    k = duckdb.connect()
    k.execute(sql).fetchall()                      # pemanasan
    waktu = []
    for _ in range(permintaan):
        t0 = time.perf_counter()
        k.execute(sql).fetchall()
        waktu.append((time.perf_counter() - t0) * 1000)
    k.close()
    return waktu

def ledakan(sql, serentak=8):
    """Ledakan permintaan serentak: 8 dashboard dibuka bersamaan."""
    def satu(_):
        k = duckdb.connect()
        t0 = time.perf_counter()
        k.execute(sql).fetchall()
        k.close()
        return (time.perf_counter() - t0) * 1000
    t0 = time.perf_counter()
    with ThreadPoolExecutor(max_workers=serentak) as pool:
        daftar = list(pool.map(satu, range(serentak)))
    dinding = (time.perf_counter() - t0) * 1000
    return dinding, max(daftar)

Driver skrip (dipotong) menjalankan ukur untuk kedua query sepuluh kali setelah pemanasan, mencetak ukuran berkas beserta median dan nilai minimum-maksimum, lalu menguji delapan permintaan serentak. Bagian penutupnya memverifikasi bahwa kedua jalur menghasilkan jawaban yang sama, lalu mengekspor mart ke CSV untuk konsumen spreadsheet:

python
a = con.execute(GMV_PER_BULAN_RAW).fetchall()
b = con.execute(GMV_PER_BULAN_MART).fetchall()
print(f"hasil kedua jalur identik: {a == b}")
print(f"contoh baris: {a[0]}")

con.execute(f"COPY (SELECT * FROM read_parquet('{MART}')) TO '{EKSPOR}' (FORMAT csv)")
baris_csv = sum(1 for _ in open(EKSPOR)) - 1
print(f"ekspor CSV mart: {baris_csv} baris, {os.path.getsize(EKSPOR)/1e3:.0f} kB")

Output yang Dihasilkan:

text
ukuran berkas : fact 8.5 MB, mart 17 kB
agregasi raw fact      : median   11.9 ms (min 11.4, max 29.8)
baca mart pre-agregat  : median    1.5 ms (min 1.4, max 2.2)
ledakan 8 dashboard serentak (waktu dinding):
  raw fact  :  228.7 ms (permintaan terlama 162.9 ms)
  dari mart :   76.9 ms (permintaan terlama 47.8 ms)
hasil kedua jalur identik: True
contoh baris: ('2026-07', 21143196250.0)
ekspor CSV mart: 2896 baris, 157 kB

Mari kita telaah bagian-bagian penting skrip di atas:

  1. Blok pertama, baris 2-9 (pre-agregasi): pernyataan COPY (... ) TO menjalankan GROUP BY tanggal, kota, status sekali dan menyimpan hasilnya — dua juta baris fact menjadi 2.896 baris mart, pekerjaan yang pada proyek nyata dijalankan model dbt terjadwal.
  2. Blok pertama, baris 11-22 (dua query konsumen): query raw memfilter lalu menjumlahkan kolom nilai dari fact; query mart menjumlahkan kolom gmv yang sudah tersimpan — filter status tetap diperlukan karena mart menyimpan ketiga status.
  3. Blok kedua, baris 1-11 (ukur): satu koneksi baru per pengukuran, pemanasan lebih dulu agar cache sistem operasi tidak memihak, sepuluh permintaan dihitung waktunya, dan median dipakai karena tahan terhadap satu lonjakan tunggal.
  4. Blok kedua, baris 13-25 (ledakan): delapan utas masing-masing membuka koneksi dan menjalankan query yang sama — meniru delapan dashboard dibuka serentak; waktu dinding mengukur keseluruhan uji serentak tersebut.
  5. Blok ketiga, baris 1-4 (verifikasi): hasil kedua jalur dibandingkan baris-per-baris; nilai True membuktikan pre-agregasi hanya memindahkan pekerjaan, bukan mengubah jawaban.

Angkanya berbicara jelas. Mart 17 kB sekitar 500 kali lebih kecil daripada fact 8,5 MB; median baca mart 1,5 ms sekitar delapan kali lebih cepat daripada agregasi raw 11,9 ms; dan saat delapan dashboard dibuka serentak, waktu dinding 76,9 ms versus 228,7 ms — konsumen terlama pun turun dari 162,9 ms ke 47,8 ms. Semua itu dicapai tanpa mengubah satu angka pun pada jawaban.

Format dan Jalur Konsumsi #

Mart disimpan sebagai Parquet karena pembacanya adalah engine SQL: format kolom terkompresi dari Chapter Parquet membuat DuckDB cukup membaca kolom yang disebut query — sintaks read_parquet dirinci di dokumentasi DuckDB , sedangkan struktur formatnya di dokumentasi Apache Parquet . Konsumen spreadsheet justru sebaliknya: tim keuangan membuka berkas di aplikasi pengolah angka, sehingga jalurnya adalah ekspor CSV — 2.896 baris, 157 kB, siap dilampirkan ke email.

Satu mart, empat jalur konsumsi dengan kebutuhan yang berbeda:

KonsumenKebutuhan utamaJalur penyajian
Dashboard BIlatensi rendah, data segarmart pre-agregat
Analis ad-hockebebasan query penuhfact dan mart di workbench
Embedded analyticsjawaban cepat di dalam aplikasimart per fitur produk
Spreadsheet timberkas mudah dibuka manusiaekspor CSV dari mart

Dari Analytics ke Machine Learning #

Section ini menuntaskan perjalanan lengkap lapisan analytics TokoKita: keputusan bentuk tabel pada Analytical Data Modeling, dbt sebagai kerangka transformasi, lapisan staging-intermediate-marts, pembaruan efisien lewat Incremental Models dan Snapshots, jaring pengaman dbt test, dokumentasi yang hidup di kode, definisi metrik tunggal pada semantic layer, integrasi alat BI lengkap dengan row-level security, dan akhirnya penyajian yang terukur. Konsumen seluruh lapisan tersebut adalah manusia: analis yang membaca grafik, manajer yang membuka laporan, tim keuangan yang menerima spreadsheet.

Jenis konsumen berikutnya bekerja dengan cara yang berbeda. Model machine learning membutuhkan dataset training yang dibekukan pada titik waktu tertentu agar evaluasi dapat diulang, fitur yang dihitung dengan cara identik saat training maupun saat inference, serta penelusuran versi data untuk setiap model yang diproduksi — kebutuhan yang tidak terjawab oleh dashboard dan mart harian. Perjalanan Course berlanjut ke section Data Engineering untuk Machine Learning, dimulai dari Chapter Kebutuhan Data Machine Learning.