Membaca data pada Spark tidak memindai seluruh berkas: parquet menyimpan skema pada footer, dan struktur direktori bulan=YYYY-MM menjadi sumber pemangkasan bahkan sebelum satu baris dibaca. Pada bagian ini kita membandingkan dua cara membaca — schema inference dan schema eksplisit — lalu mengukur manfaat partition pruning pada filter bulan tunggal. Seluruh angka diukur nyata pada mesin uji dua CPU dengan local[1]; datanya dataset TokoKita 22,6 MB. Materi pendukung ada pada chapter Spark DataFrames dan Parquet.

Tanpa deklarasi, Spark membaca footer parquet untuk menebak tipe setiap kolom. Proses ini tetap membuka dan mengurai footer dari seluruh berkas (24 direktori partition pada tabel pesanan):

Output yang Dihasilkan:

text
== schema inference (baca footer parquet) ==
root
 |-- pesanan_id: string (nullable = true)
 |-- tanggal: date (nullable = true)
 |-- pelanggan_id: long (nullable = true)
 |-- kanal: string (nullable = true)
 |-- status: string (nullable = true)
 |-- diskon_rp: long (nullable = true)
 |-- bulan: string (nullable = true)

Ketika skema sudah kita ketahui — misalnya pada pipeline yang dijalankan rutin — deklarasi eksplisit memakai StructType menghilangkan langkah penebakan. Deklarasi berikut diambil dari skrip demo:

python
SKEMA_PESANAN_EKSPLISIT = StructType(
    [
        StructField("pesanan_id", StringType(), False),
        StructField("tanggal", DateType(), True),
        StructField("pelanggan_id", IntegerType(), True),
        StructField("kanal", StringType(), True),
        StructField("status", StringType(), True),
        StructField("diskon_rp", IntegerType(), True),
        StructField("bulan", StringType(), True),
    ]
)

Mari kita telaah baris-baris kode di atas:

  1. Baris 3 (pesanan_id, False): kunci bisnis dideklarasikan tidak nullable — pernyataan kontrak, bukan tebakan; filter dan join pada kunci ini kemudian boleh melewati pemeriksaan null.
  2. Baris 5 (pelanggan_id, IntegerType): tipe sengaja diturunkan dari long (hasil inference) menjadi integer karena 60 ribu pelanggan jauh di bawah jangkauan int; memori per baris turun separuhnya.
  3. Baris 6-8 (nullable=True): kolom lain tetap dideklarasikan nullable — jujur terhadap kenyataan bahwa status dan diskon_rp memang bisa kosong pada sistem sumber.

Output yang Dihasilkan:

text
== schema eksplisit (StructType) ==
root
 |-- pesanan_id: string (nullable = true)
 |-- tanggal: date (nullable = true)
 |-- pelanggan_id: integer (nullable = true)
 |-- kanal: string (nullable = true)
 |-- status: string (nullable = true)
 |-- diskon_rp: integer (nullable = true)
 |-- bulan: string (nullable = true)

skema sama: False

Baris skema sama: False disengaja: df_infer.schema == df_eksplisit.schema membandingkan tipe per kolom, dan long hasil inference memang berbeda dengan integer yang kita deklarasikan. Perbandingan waktu baca (dua kali, urutan dibalik agar adil):

Output yang Dihasilkan:

text
== waktu baca: inference vs eksplisit (dua kali, urutan dibalik) ==
  inference-1  2.085 s
  eksplisit-1  0.384 s
  eksplisit-2  0.264 s
  inference-2  0.385 s

Angka inference-1 yang besar mencakup penebakan skema 24 berkas pada pembacaan pertama; setelah berkas berada di cache halaman sistem operasi, keduanya sekitar 0,3 detik. Pada pipeline produksi yang membaca ratusan ribu berkas, selisih inilah yang dikembalikan menjadi waktu startup setiap run.

Partition Pruning: Memangkas di Tingkat Direktori #

Filter pada kolom partition dipangkas sebelum pembacaan: Spark hanya membuka direktori bulan=2025-12 dan melewati 23 direktori lainnya. Perbandingannya:

Output yang Dihasilkan:

text
== partition pruning: pindai penuh vs satu bulan ==
  seluruh 24 bulan :   800,000 baris dalam 0.355 s
  bulan=2025-12    :    33,914 baris dalam 0.154 s
  perbandingan waktu penuh/pruning: 2.3

Bukti pemangkasan tidak berada pada jam, melainkan pada query plan. Baris PartitionFilters berikut menunjukkan bahwa predikat bulan = 2025-12 diterapkan pada tingkat direktori — topik Spark Partitioning:

Output yang Dihasilkan:

text
== bukti pada query plan (PartitionFilters) ==
  PartitionFilters: [isnotnull(bulan#92), (bulan#92 = 2025-12)]

Filter gabungan partition dan kolom biasa bekerja di dua lapis: bulan dipangkas di tingkat direktori, status didorong ke pembaca parquet (Pushed Filter, kita lihat buktinya pada Bagian 7 dan 8). Hasilnya untuk November 2025:

Output yang Dihasilkan:

text
== baris terfilter untuk November 2025 ==
+-----------+----------+------------+--------+-------+---------+-------+
|pesanan_id |tanggal   |pelanggan_id|kanal   |status |diskon_rp|bulan  |
+-----------+----------+------------+--------+-------+---------+-------+
|ORD-0000671|2025-11-01|8711        |aplikasi|selesai|4000     |2025-11|
|ORD-0000672|2025-11-02|8724        |aplikasi|selesai|0        |2025-11|
|ORD-0000673|2025-11-03|8737        |aplikasi|selesai|0        |2025-11|
|ORD-0000674|2025-11-04|8750        |aplikasi|selesai|0        |2025-11|
|ORD-0000675|2025-11-05|8763        |aplikasi|selesai|0        |2025-11|
+-----------+----------+------------+--------+-------+---------+-------+
only showing top 5 rows

Data kini berada di tangan Spark sebagai DataFrame yang siap diolah. Berikutnya kita mengubah isinya: kolom turunan, klasifikasi bertingkat, penanganan null, dan perbandingan UDF melawan fungsi built-in — tugas Bagian 4.