Optimasi tanpa pengukuran hanya spekulasi, jadi pada bagian ini setiap klaim diuji dengan angka nyata dari mesin uji dua CPU local[1]. Perlu diakui sejak awal: dataset kita hanya 22,6 MB dan berkasnya hangat di cache halaman sistem operasi, sehingga rasio yang terbaca di sini cenderung kecil — pada data yang lebih besar perbandingannya melebar. Empat pengukuran dilakukan berurutan: cache dingin lawan panas, column pruning, predicate pushdown, lalu gabungan ketiganya pada satu query. Materi pendukung ada pada chapter Spark Performance dan Shuffle dan Skew.
Cache: Dingin vs Panas #
Fungsi efek_cache pada src/optimasi.py mengukur aksi yang sama dua kali — sebelum dan sesudah cache:
def efek_cache(df, aksi=None):
"""Ukur aksi pada kondisi dingin (tanpa cache) vs panas (setelah cache).
`aksi` default: hitung SUM(total). Cache dibersihkan dengan `unpersist`
supaya pengukuran dingin berikutnya benar-benar membaca sumber.
"""
if aksi is None:
def aksi(d):
return d.select(F.sum("total")).head()[0]
dingin = ukur(lambda: aksi(df))
df.cache()
df.count() # materialisasi cache
panas = ukur(lambda: aksi(df))
terpasang = df.is_cached
df.unpersist()
return {"dingin_s": dingin, "panas_s": panas, "terpasang_saat_ukur": terpasang}Mari kita telaah baris-baris kode di atas:
- Baris 14 (
dingin): aksi dijalankan sebelum cache; DataFramedfadalah gabungan tiga tabel hasil Bagian 5, jadi pembacaan dingin mengulang join dan pembacaan parquet. - Baris 15-16 (
cache+count):cachehanya mendaftarkan niat;countmemaksa materialisasi sehingga hasil benar-benar tersimpan di memori. - Baris 17 (
panas): aksi yang sama kini membaca blok memori;is_cacheddicatat sebagai bukti, bukan asumsi. - Baris 19 (
unpersist): cache dilepas supaya pengukuran dingin berikutnya benar-benar membaca sumber.
Hasilnya dua putaran berturut-turut:
Output yang Dihasilkan:
== 1. efek cache pada agregasi bulanan (dingin vs panas) ==
putaran 1: {'dingin_s': 5.078, 'panas_s': 0.551, 'terpasang_saat_ukur': True}
putaran 2: {'dingin_s': 2.112, 'panas_s': 0.206, 'terpasang_saat_ukur': True}Putaran pertama: 5,078 detik turun menjadi 0,551 detik (sekitar sembilan kali lebih cepat). Putaran kedua menunjukkan dua hal sekaligus. dingin turun menjadi 2,112 detik karena berkas parquet dan kode JIT sudah hangat dari putaran pertama — “dingin” di sini hanya melepas cache Spark, bukan mendinginkan seluruh tumpukan. Sementara panas turun menjadi 0,206 detik karena blok cache sudah tersusun rapi. Pelajaran praktisnya: jika sebuah DataFrame dipakai berulang (misalnya oleh beberapa agregasi berbeda), cache membayar dirinya sendiri sejak aksi kedua.
Column Pruning pada Pembacaan Parquet #
Parquet menyimpan data per kolom, jadi memilih dua kolom dari tujuh seharusnya membaca byte lebih sedikit:
Output yang Dihasilkan:
== 2. column pruning pada pembacaan parquet ==
seluruh 7 kolom : 0.206 s (rata-rata 3x)
2 kolom terpilih: 0.149 s (rata-rata 3x)
kolom yang dibaca Scan: +- Scan parquet (1)Selisihnya 0,057 detik — nyata namun kecil, dan itu jujur: tabel pesanan hanya 7,6 MB sehingga waktu dominannya adalah penjadwalan task, bukan pembacaan disk. Bukti strukturalnya ada pada baris kolom yang dibaca Scan: rencana hanya memuat kolom yang diminta, sama seperti operator (4) pada Bagian 7 yang membaca tiga kolom padahal tabelnya tujuh. Pada tabel lebar (ratusan kolom, data lake nyata), pemangkasan kolom adalah salah satu optimasi termurah yang tersedia.
Predicate Pushdown: Filter Partition dan Kolom #
Pengukuran ketiga membandingkan count penuh lawan count dengan filter bulan = 2025-12 dan status = selesai:
Output yang Dihasilkan:
== 3. predicate pushdown (filter partition + kolom) ==
hasil: {'pindai_penuh_s': 0.17, 'terfilter_s': 0.167, 'baris_penuh': 800000, 'baris_terfilter': 28829}
bukti pada plan:
PartitionFilters: [isnotnull(bulan#1630), (bulan#1630 = 2025-12)]
PushedFilters: [IsNotNull(status), EqualTo(status,selesai)]Di sinilah kejujuran angka kecil penting: waktu berjalan hampir sama (0,17 lawan 0,167 detik) pada mesin satu inti, karena biaya penjadwalan task membatasi keduanya. Namun jumlah baris yang disentuh berbeda 28 kali — 800.000 lawan 28.829 — dan dua penanda plan menunjukkan bahwa 23 direktori bulan tidak dibuka (PartitionFilters) serta predikat status dievaluasi pada row group (PushedFilters). Pada kluster dengan data ratusan gigabita, penghematan inilah yang menentukan; pada dataset kecil, penandanya yang tetap harus kita periksa.
Perbandingan ketiga ini juga menegaskan urutan membaca bukti. Angka baris dibaca lebih dulu (baris_penuh lawan baris_terfilter) karena angka tersebut paling tahan terhadap derau penjadwalan; penanda plan dibaca kedua sebagai penjelasan mekanisme; waktu berjalan dibaca terakhir dan diperlakukan sebagai hasil, bukan sumber kebenaran.
Gabungan Teknik pada Satu Query #
Terakhir, ketiga teknik dipakai bersama pada satu query analitik: GMV Desember 2025 per kategori. Filter partition memangkas direktori, proyeksi memilih kolom, dan orderBy menutup dengan sort kecil:
Output yang Dihasilkan:
== 4. gabungan teknik pada agregasi kategori ==
+---------------+----------+
|kategori |gmv_des |
+---------------+----------+
|alat seduh |5829784000|
|kopi bubuk |2114081000|
|perlengkapan |1841052000|
|kopi saset |1504998000|
|sirup |1435175000|
|susu dan krimer|1176959000|
|teh |848624000 |
|cemilan |646710000 |
+---------------+----------+
durasi: 1.54 sAngka 1,54 detik mencakup pembacaan potongan Desember, join ke dimensi produk, agregasi delapan kategori, sampai pencetakan tabel. Bandingkan dengan pengukuran agregasi bulanan + show 5,40 detik pada Bagian 5 yang memproses 24 bulan dengan countDistinct — memangkas cakupan data sejak awal memberi penghematan terbesar, jauh sebelum cache atau tuning lain bekerja.
Urutan prioritas yang terbentuk dari keempat pengukuran ini: pangkas cakupan data (filter partition) lebih dulu, pilih kolom secukupnya, cache hanya untuk hasil yang dipakai berulang. Seluruh kunci konfigurasi yang menopang mekanisme tersebut terdokumentasi di https://spark.apache.org/docs/latest/configuration.html; panduan tuning lengkap ada di https://spark.apache.org/docs/latest/ .
Semua teknik optimasi telah terukur. Satu langkah tersisa untuk menutup proyek: menuliskan hasil agregat ke penyimpanan dengan benar — partition, mode overwrite, dan verifikasi baca ulang — tugas Bagian 9.