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:

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

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

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

  1. Baris 38-39 (warm-up): satu run dibuang; tanpa ini run pertama hampir selalu tampak sebagai run terburuk dan membiaskan kesimpulan.
  2. Baris 44 (urutan asli): daftar runs sengaja tidak diurutkan — sebaran waktu asli diperlukan untuk mendeteksi pola seperti pemanasan bertahap atau gangguan di tengah pengukuran.
  3. Baris 46 (p95): persentil dihitung dari salinan terurut dengan indeks round(0.95 * n) - 1, estimasi sederhana yang stabil untuk N kecil.
  4. Baris 47-49 (lebar): rentang maksimum/minimum dalam persen mengukur kestabilan; rentang lebar berarti angka belum layak dipakai untuk klaim perbedaan kecil.
  5. 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:

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

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

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

Fungsi 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.