Parquet adalah format penyimpanan kolom yang menjadi pilihan baku untuk data analitik pada pipeline Spark, dan memahami cara kerjanya menjelaskan mengapa pembacaan yang dirancang baik terasa jauh lebih cepat. Topik ini membahas perbedaan penyimpanan baris dan kolom, mekanisme predicate pushdown dan pemangkasan kolom yang terlihat pada rencana eksekusi, serta peran kompresi per kolom. Setelah membacanya, struktur keluaran Parquet dapat dibaca, dan rencana pembacaannya dapat ditafsirkan untuk memastikan Spark tidak membaca lebih dari yang diperlukan.
Penyimpanan baris versus penyimpanan kolom#
Format baris menyimpan satu catatan lengkap setelah catatan lain, sedangkan format kolom menyimpan nilai satu kolom berurutan dalam satu blok, lalu berpindah ke kolom berikutnya. Sebuah row group memuat sepotong baris yang disimpan per kolom di dalamnya, dan setiap blok kolom membawa statistik seperti nilai minimum dan maksimum. Susunan inilah yang membuat format kolom unggul untuk analitik, karena kueri biasanya hanya membutuhkan beberapa kolom dari puluhan yang ada.
Untuk pekerjaan Spark, keunggulan itu terwujud dalam dua bentuk penghematan sekaligus. Bacaan hanya mengangkat kolom yang dirujuk, sehingga jumlah byte dari disk berkurang, dan kompresi per kolom bekerja jauh lebih baik karena nilai sesama kolom seragam, misalnya ribuan teks kategori yang mirip atau angka dengan rentang sempit. Konsekuensinya, data Parquet yang isinya sama dengan CSV biasanya berukuran beberapa kali lebih kecil, dan pembacaannya lebih sedikit menyentuh disk.
Membuktikan pushdown pada rencana eksekusi#
Predicate pushdown berarti filter yang ditulis di API ditekan sampai ke lapisan pembacaan berkas, sehingga baris yang tidak mungkin lolos dibuang saat pembacaan berlangsung, bukan setelah seluruh kolom diangkat. Pada Parquet, pembaca memanfaatkan statistik row group untuk melewati blok yang tidak mungkin memuat baris lolos, dan menyaring nilai di dalam blok yang terpilih. Skrip berikut menulis tabel kecil ke Parquet lalu mencetak rencana pembacaan satu kolom dengan satu filter.
from pyspark.sql import SparkSession
from pyspark.sql import functions as F
spark = (
SparkSession.builder
.master("local[2]")
.appName("peraga-parquet")
.config("spark.sql.shuffle.partitions", "2")
.getOrCreate()
)
df = spark.createDataFrame(
[(1, "Ali", "10A", 85), (2, "Budi", "10A", 90), (3, "Citra", "10B", 78), (4, "Dewi", "10B", 92)],
"id long, nama string, kelas string, nilai long",
)
df.write.mode("overwrite").parquet("/tmp/lokasi/pq_ujian")
baca = spark.read.parquet("/tmp/lokasi/pq_ujian")
print("=== rencana baca satu kolom + filter ===")
baca.select("kelas").where(F.col("nilai") >= 80).explain()
spark.stop()WARNING: Using incubator modules: jdk.incubator.vector
=== rencana baca satu kolom + filter ===
== Physical Plan ==
*(1) Project [kelas#6]
+- *(1) Filter (isnotnull(nilai#7L) AND (nilai#7L >= 80))
+- *(1) ColumnarToRow
+- FileScan parquet [kelas#6,nilai#7L] Batched: true, DataFilters: [isnotnull(nilai#7L), (nilai#7L >= 80)], Format: Parquet, Location: InMemoryFileIndex(1 paths)[file:/tmp/lokasi/pq_ujian], PartitionFilters: [], PushedFilters: [IsNotNull(nilai), GreaterThanOrEqual(nilai,80)], ReadSchema: struct<kelas:string,nilai:bigint>Rencana ini menjadi bukti empiris atas pembahasan di atas. Bagian FileScan parquet hanya memuat kelas dan nilai pada ReadSchema, bukan id dan nama, dan itulah pemangkasan kolom yang menurunkan byte dari disk. Bagian PushedFilters memuat syarat IsNotNull(nilai) dan GreaterThanOrEqual(nilai,80), yang berarti filter sudah diserahkan ke pembaca Parquet untuk memanfaatkan statistik blok, dan bagian DataFilters menunjukkan filter yang tetap dievaluasi pada lapisan Spark sebagai jaminan kebenaran. Kolom id dan nama tidak pernah dibaca meskipun berada di berkas yang sama.
Perlu dicatat bahwa yang ditekan ke pembaca hanyalah filter sederhana atas kolom yang tersimpan. Filter yang melibatkan fungsi rumit atau kolom hasil perhitungan tetap dievaluasi Spark setelah bacaan. Pemangkasan kolom hanya berlaku pada kolom yang benar-benar dirujuk di atas action, sehingga kebiasaan menyaring kolom sejak awal, yang diperkenalkan pada modul transformasi, bekerja sama dengan sifat format ini.
Kompresi dan struktur berkas#
Kompresi bawaan Parquet pada Spark adalah snappy, yang mengutamakan kecepatan membongkar daripada ukuran hasil terkecil, dan pilihan lain seperti gzip atau zstd dapat diatur melalui opsi penulisan. Karena kompresi bekerja per kolom di dalam row group, kolom yang isinya seragam menyusut jauh lebih dalam daripada kolom yang isinya acak, dan pembacaan kolom tunggal tidak harus membongkar kolom lain.
Keluaran penulisan Parquet berupa folder yang berisi berkas part-*.parquet beserta berkas penanda _SUCCESS, dan setiap berkas partisi membawa schema di dalam footernya. Struktur folder inilah yang menjadi dasar pembacaan paralel, karena setiap berkas partisi dapat dibaca oleh task yang berbeda, dan topik penulisan terpartisi melanjutkan pembahasan ini dengan menyusun folder berdasarkan isi kolom.
Kesalahan umum#
Kesalahan pertama adalah membaca tabel Parquet dengan select * padahal hanya beberapa kolom yang dibutuhkan. Pemangkasan kolom hanya efektif ketika sebagian kolom tidak dirujuk, sehingga menyebut semuanya membuat pembacaan mengangkat seluruh isi berkas. Tulis daftar kolom yang diperlukan secara eksplisit pada rantai transformasi.
Kesalahan kedua adalah menyimpan data perantara dalam CSV dengan alasan kemudahan, lalu menanggung konsekuensinya pada setiap pembacaan berupa inferensi tipe dan byte yang lebih besar. Untuk data perantara antar langkah pipeline, Parquet menyimpan schema beserta datanya, dan pembandingan biayanya sudah dibahas pada topik membaca data.
Kesalahan ketiga adalah menganggap semua filter otomatis tertekan ke pembaca. Filter sederhana atas kolom yang tersimpan adalah yang paling aman ditekan, sedangkan filter atas ekspresi rumit dievaluasi Spark setelah bacaan. Periksa bagian PushedFilters pada rencana bila kepastian diperlukan, dan jangan menyimpulkan dari nama metode filternya saja.
Ringkasan#
- Parquet menyimpan data per kolom dalam row group dengan statistik per blok, sehingga bacaan kolom terpilih dan kompresi per kolom menjadi efisien.
- Predicate pushdown menyerahkan filter sederhana ke pembaca berkas dan terlihat pada
PushedFilters, sedangkan pemangkasan kolom terlihat padaReadSchemarencana eksekusi. - Kompresi bawaan snappy mengutamakan kecepatan, dan keluaran penulisan berupa folder berkas partisi dengan schema di dalam footernya.
- Filter rumit tetap dievaluasi Spark setelah bacaan, sehingga bagian
PushedFilterspada rencana adalah bukti yang benar, bukan asumsi.
Referensi#
- Parquet Files — Apache Spark — Dokumentasi resmi pembacaan dan penulisan Parquet beserta opsinya.
- Apache Parquet Format — Spesifikasi format, row group, dan metadata statistiknya.
- pyspark.sql.DataFrame.explain — Apache Spark — Cara membaca rencana eksekusi yang memuat PushedFilters dan ReadSchema.