Klaim “DuckDB sepuluh kali lebih cepat dari pandas” tanpa disertai metode bukan informasi — sepuluh kali lebih cepat pada data apa, mesin apa, diukur berapa kali, dan dengan cache hangat atau dingin? Chapter Kapan Menggunakan Spark memperlihatkan satu perbandingan yang jujur; bab ini membakukan caranya menjadi metodologi: aturan pertandingan yang adil, warm-up, pengulangan N kali dengan statistik lengkap, dan pelaporan yang membuat orang lain bisa mengulang pengukuran kita. Bab penutup section ini sekaligus menjadi tempat semua teknik sebelumnya — dari parallelism sampai tiering storage — bertemu: semuanya pada akhirnya berdiri di atas angka yang diukur dengan benar.
Aturan Pertandingan yang Adil #
Empat aturan membuat dua peserta bisa dibandingkan: data dan kolom sama persis; hasil diverifikasi identik sebelum waktunya dibandingkan; run pertama dibuang sebagai warm-up karena import modul dan page cache membebani run pertama; dan tiap peserta dijalankan N kali sehingga sebaran alaminya terlihat. Fungsi benchmark berikut membungkus keempatnya:
def benchmark(nama, fungsi, *, warmup=1, n=7):
"""Harness: buang warm-up, jalankan n kali, laporkan min/median/p95."""
for _ in range(warmup):
fungsi()
runs = []
for _ in range(n):
t0 = time.perf_counter()
hasil = fungsi()
runs.append(time.perf_counter() - t0) # urutan asli dipertahankan
terurut = sorted(runs)
p95 = terurut[max(0, round(0.95 * n) - 1)]
return {"nama": nama, "min": terurut[0], "median": statistics.median(runs),
"p95": p95, "lebar": (terurut[-1] / terurut[0] - 1) * 100,
"runs": runs, "hasil": hasil}Dua peserta kemudian mengerjakan tugas yang sama — GROUP BY kota pada 5 juta baris Parquet — lewat jalur yang berbeda:
def tugas_duckdb():
return con.execute(f"""
SELECT kota, count(*) AS jumlah, sum(harga) AS gmv
FROM read_parquet('{BERKAS}') GROUP BY kota ORDER BY kota
""").fetchall()
def tugas_pandas():
import pandas as pd
df = pd.read_parquet(BERKAS)
hasil = df.groupby("kota", sort=True).agg(
jumlah=("pesanan_id", "size"), gmv=("harga", "sum"))
return [(k, int(r.jumlah), int(r.gmv)) for k, r in hasil.iterrows()]Output yang Dihasilkan:
ukuran data: 47 MB, 5.000.000 baris, kolom sama untuk kedua peserta
peserta min median p95 rentang
-------------------------------------------------------
DuckDB groupby 0.058s 0.071s 0.079s 37.4%
pandas groupby 0.505s 0.522s 0.555s 10.0%
hasil kedua peserta identik: True
contoh hasil: ('Bandung', 624442, 235568627623)
jebakan single-run: 7 run DuckDB dalam urutan eksekusi sebenarnya -
['0.071', '0.079', '0.058', '0.071', '0.075', '0.058', '0.068']
bila hanya run pertama yang diambil: 0.071s (+23% dari kemampuan sebenarnya 0.058s)Mari kita telaah baris-baris kode di atas:
- Baris 38-39 (warm-up): satu run dibuang; tanpa ini run pertama hampir selalu tampak sebagai run terburuk dan membiaskan kesimpulan.
- Baris 44 (urutan asli): daftar
runssengaja tidak diurutkan — sebaran waktu asli diperlukan untuk mendeteksi pola seperti pemanasan bertahap atau gangguan di tengah pengukuran. - Baris 46 (p95): persentil dihitung dari salinan terurut dengan indeks
round(0.95 * n) - 1, estimasi sederhana yang stabil untuk N kecil. - Baris 47-49 (
lebar): rentang maksimum/minimum dalam persen mengukur kestabilan; rentang lebar berarti angka belum layak dipakai untuk klaim perbedaan kecil. - Baris 52-56 versus 59-64 (dua tugas): kedua fungsi mengembalikan struktur data identik sehingga hasil bisa diverifikasi sama sebelum waktunya diperbandingkan.
DuckDB menyelesaikan groupby dengan median 0,071 detik; pandas 0,522 detik — sekitar tujuh kali lebih lambat pada mesin uji ini. Perhatikan pula rentangnya: DuckDB 37,4% jauh lebih lebar daripada pandas 10,0%, karena jalur yang lebih cepat biasanya juga lebih sensitif terhadap derau penjadwalan. Karena itu laporan yang jujur memuat minimum, median, p95, dan rentang sekaligus — satu angka tunggal menyembunyikan informasi yang dibutuhkan untuk menilai klaim.
Jebakan Single-Run #
Bagian bawah output di atas mencetak tujuh run DuckDB dalam urutan eksekusi sebenarnya: 0,071 lalu 0,079 lalu 0,058, dan seterusnya. Bila laporan hanya mengambil satu run dan kebetulan mengambil run pertama, klaimnya menjadi 0,071 detik — 23% lebih lambat dari kemampuan sebenarnya (0,058 detik). Ketidakpastian sebesar ini menelan seluruh ruang banyak optimasi: perbaikan 15% mustahil terlihat dari satu run. Chapter Mengukur Performa Pipeline menyebut gejala ini sebagai pita noise dan menyelesaikannya dengan baseline; harness ini mengukur pitanya secara eksplisit lewat kolom rentang.
Jebakan Cache Dingin #
Jebakan kedua bekerja berlawanan arah: run pertama dianggap mewakili keadaan nyata. Akses berkas pertama membayar pembacaan disk; run berikutnya membaca dari page cache OS yang jauh lebih cepat. Skrip berikut memakai posix_fadvise untuk membuang berkas 112 MB dari page cache sebelum tiap run dingin:
def evict_page_cache(path):
"""Minta OS membuang berkas dari page cache (POSIX_FADV_DONTNEED)."""
fd = os.open(path, os.O_RDONLY)
os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED)
os.close(fd) print("cache dingin (page cache dibuang sebelum tiap run):")
dingin = []
for _ in range(3):
evict_page_cache(BERKAS)
t0 = time.perf_counter()
con.execute(query).fetchall()
dingin.append(time.perf_counter() - t0)
print(" ", [f"{d:.3f}s" for d in dingin])
print("cache hangat (berkas sudah di page cache):")
hangat = []
for _ in range(3):
t0 = time.perf_counter()
con.execute(query).fetchall()
hangat.append(time.perf_counter() - t0)
print(" ", [f"{d:.3f}s" for d in hangat])
print(f"run dingin pertama {dingin[0]:.3f}s vs terbaik hangat "
f"{min(hangat):.3f}s "
f"(+{(dingin[0] / min(hangat) - 1) * 100:.0f}% bila dilaporkan sendirian)")Output yang Dihasilkan:
ukuran berkas: 112 MB
cache dingin (page cache dibuang sebelum tiap run):
['0.214s', '0.208s', '0.209s']
cache hangat (berkas sudah di page cache):
['0.146s', '0.168s', '0.145s']
run dingin pertama 0.214s vs terbaik hangat 0.145s (+47% bila dilaporkan sendirian)Run dingin konsisten di 0,208-0,214 detik; run hangat di 0,145-0,168 detik. Selisih 47% itu bukan kecepatan DuckDB yang berubah, melainkan lokasi byte yang dibaca: disk atau RAM. Keputusan pelaporannya bergantung pada pertanyaan: pengguna interaktif yang menanyakan pertama kali setelah pipeline selesai memang mengalami angka dingin, sehingga benchmark interaktif melaporkan keduanya; benchmark throughput pipeline yang membaca berulang justru wajib menghangatkan cache dulu agar tidak mengukur kecepatan disk.
Template Laporan Benchmark #
Angka tanpa konteks tidak bisa direproduksi orang lain. Template minimum untuk laporan yang jujur:
Laporan : DuckDB vs pandas — groupby 5 juta baris
Lingkungan : 2 CPU, 4 GB RAM, Python 3.12, DuckDB 1.5.6, pandas 2.2.3
Data : pesanan-5juta.parquet, 47 MB, 3 kolom, 5.000.000 baris
Prosedur : warm-up 1 run dibuang; 7 run per peserta; cache hangat
Hasil : DuckDB min 0.058s median 0.071s p95 0.079s rentang 37.4%
pandas min 0.505s median 0.522s p95 0.555s rentang 10.0%
Verifikasi : hasil kedua peserta identik (Bandung 624.442 baris)
Kesimpulan : DuckDB ~7x lebih cepat di median pada beban dan mesin ini;
klaim tidak berlaku untuk data/join/mesin lain tanpa diukur ulangFungsi statistik dan pewaktu yang dipakai harness terdokumentasi di dokumentasi statistics dan time.perf_counter . Dengan ini perjalanan section Optimasi dan Performa tuntas: mengukur, memprofil, mendorong filter dan kolom, menyetel batch, memparalelkan, men-cache, menghemat memori, meluruskan skew, dan menekan biaya storage — semuanya dengan angka yang bisa dipertanggungjawabkan. Langkah berikutnya meninggalkan level taktis menuju gambar besar: bagaimana pipeline-pipeline optimal ini disusun menjadi arsitektur utuh, dibuka oleh section Arsitektur Data pada chapter Layered Data Architecture.