Hampir setiap tim data pernah mengalami hal ini: file CSV dan JSON yang awalnya kecil berubah menjadi ratusan gigabyte, lalu query ...
Hampir setiap tim data pernah mengalami hal ini: file CSV dan JSON yang awalnya kecil berubah menjadi ratusan gigabyte, lalu query analitik yang seharusnya selesai dalam hitungan detik mulai memakan waktu menit. Masalahnya sering bukan mesin query, melainkan format penyimpanan. CSV menyimpan data baris demi baris, tanpa tipe yang tegas, tanpa statistik, dan tanpa cara melewati bagian file yang tidak relevan. Apache Parquet dirancang untuk menutup celah itu, dan hari ini ia menjadi format file yang paling umum dijumpai di ekosistem analitik, mulai dari Spark, Trino, DuckDB, hingga pandas dan Polars.
Artikel ini membahas Parquet dari sisi praktis: apa yang disimpan di dalam file, mengapa tata letak kolumnar mempercepat query analitik, bagaimana skema dan partisi disusun, serta jebakan yang muncul saat ia dipakai sehari-hari. Contohnya pola laboratorium lokal berskala kecil, dan bagian akhir menyebut apa yang belum diuji. Tidak ada angka benchmark di sini, karena performa Parquet bergantung pada bentuk data, ukuran row group, codec, dan mesin pembacanya.
Apa Itu Apache Parquet dan Masalah yang Ia Jawab
Parquet adalah format file biner terbuka untuk data tabular, awalnya dikembangkan di ekosistem Hadoop dan sekarang menjadi proyek di bawah Apache Software Foundation. Spesifikasinya terbuka, sehingga banyak bahasa dan mesin dapat membaca maupun menulisnya tanpa bergantung pada satu vendor. File Parquet bersifat immutable: sekali ditulis, isinya tidak diubah di tempat. Pembaruan dilakukan dengan menulis file baru, dan penghapusan berarti membuang file atau menulis ulang himpunan data.
Perbedaan mendasar dengan CSV terletak pada tiga hal. Pertama, Parquet menyimpan skema: setiap kolom punya nama dan tipe yang eksplisit, termasuk tipe logis seperti tanggal, timestamp, dan desimal. Kedua, data disimpan per kolom, bukan per baris. Ketiga, file membawa metadata di bagian akhir, mencakup statistik minimum dan maksimum per kolom di setiap row group. Ketiga sifat ini yang membuat pembacaan selektif menjadi mungkin: mesin query bisa mengambil hanya kolom yang dibutuhkan dan melewati row group yang tidak memuat nilai yang dicari.
Parquet juga mendukung data bersarang (nested) melalui struktur group dan list, warisan kebutuhan menyimpan record bergaya JSON tanpa meratakannya lebih dulu. Bagi tim yang mengelola event log atau data semi-terstruktur, di sinilah nilai Parquet terasa. Dokumentasi resminya di parquet.apache.org/docs menjadi rujukan untuk detail format yang tidak dibahas tuntas di sini.
Penyimpanan Kolumnar: Mengapa Lebih Efisien untuk Analitik
Query analitik jarang membaca seluruh kolom. Laporan penjualan mungkin hanya menyentuh tanggal, wilayah, dan total, sementara tabel sumber punya tiga puluh kolom lain. Pada penyimpanan baris, semua kolom tetap dibaca karena satu baris disimpan berurutan. Pada penyimpanan kolom, pembaca dapat melompat langsung ke kolom yang diminta. Inilah keuntungan pertama dan paling sering dikutip: column projection.
Keuntungan kedua muncul dari sifat data itu sendiri. Nilai dalam satu kolom biasanya sejenis dan berulang, misalnya kode negara atau status pesanan, sehingga sangat ramah terhadap kompresi. Parquet memakai dictionary encoding untuk nilai berulang, run-length encoding untuk urutan nilai sama, dan bit-packing untuk bilangan bulat kecil. Codec seperti Snappy, gzip, Zstandard, LZ4, dan Brotli diterapkan di atas hasil encoding itu. Kombinasi keduanya membuat file Parquet lebih ringkas daripada CSV berisi setara, meski besarnya penghematan sangat bergantung pada data.
Row Group, Page, dan Statistik
File Parquet tidak disimpan sebagai satu blok besar. Isinya dipecah menjadi row group, dan setiap row group dipecah lagi menjadi page per kolom. Row group adalah unit dasar pembacaan paralel dan unit tempat statistik disimpan. Ketika mesin query mencari status = 500, ia dapat memeriksa statistik minimum dan maksimum kolom tersebut di setiap row group, lalu melewati row group yang seluruh nilainya di luar rentang pencarian. Teknik ini disebut predicate pushdown, dan efektivitasnya bergantung pada seberapa baik data terurut serta seberapa besar row group.
Ukuran row group adalah kompromi. Row group sangat kecil menambah overhead metadata dan memperlambat pembacaan berurutan; row group sangat besar mengurangi keuntungan melewati data dan menaikkan kebutuhan memori. pyarrow memberi kendali melalui row_group_size, sementara banyak mesin memilih nilai bawaan yang wajar. Untuk eksperimen lokal, mulailah dari nilai bawaan, ukur pada data nyata, lalu ubah satu parameter pada satu waktu.
Skema dan Evolusi Skema
Karena skema tersimpan di dalam file, Parquet tidak perlu membaca seluruh isi untuk mengetahui nama dan tipe kolom; metadata di footer sudah cukup. Operasi seperti menghitung baris, memeriksa kolom, atau memilih subset kolom pun menjadi murah, bahkan pada file besar.
import pyarrow.parquet as pq
pf = pq.ParquetFile("data/events/date=2026-11-12/part-000.parquet")
print(pf.schema_arrow) # skema logis yang dibaca Arrow
print(pf.metadata.num_rows) # jumlah baris
print(pf.metadata.num_row_groups)
rg = pf.metadata.row_group(0)
for i in range(rg.num_columns):
kolom = rg.column(i)
stats = kolom.statistics
if stats is not None and stats.has_min_max:
print(kolom.path_in_schema, stats.min, stats.max)
Perhatikan bahwa statistik bisa saja tidak ada: penulis file boleh tidak menyimpannya, dan untuk tipe tertentu statistik tidak bermakna. Kode yang membaca metadata harus memeriksa ketersediaannya, bukan mengasumsikan. Parquet memberi banyak informasi, tetapi tidak semuanya dijamin ada di setiap file.
Menambah Kolom, Mengubah Nama, dan Keterbatasan
Evolusi skema paling aman dilakukan dengan menambah kolom opsional. File lama tidak perlu ditulis ulang; pembaca mengisi null untuk kolom yang tidak ada. Mengganti nama kolom lebih rumit: banyak pustaka memetakan kolom berdasarkan nama, sehingga rename dianggap kolom baru dan kolom lama hilang. Spesifikasi mengenal pengenal field, tetapi tidak semua penulis mengisinya dan tidak semua pembaca memakainya. Dalam praktik, rename sebaiknya diperlakukan sebagai perubahan terkoordinasi, bukan operasi sepele.
Mengubah tipe kolom lebih berisiko: menaikkan presisi desimal atau mengubah string menjadi timestamp biasanya memerlukan penulisan ulang data. Format tabel seperti Iceberg dan Delta Lake menambahkan metadata di atas Parquet untuk menangani evolusi skema lebih terkendali; keduanya di luar cakupan artikel ini, tetapi penting diketahui bahwa Parquet sendirian bukan solusi tata kelola skema.
Partisi dan Tata Letak Direktori di Lab Lokal
Parquet adalah format file, bukan format tabel; ia tidak mengatur susunan direktori. Konvensi yang paling banyak dipakai adalah partisi bergaya Hive, yaitu direktori bernama kolom=nilai. Mesin query mengenali pola ini sebagai kolom virtual, sehingga filter pada kolom partisi dapat melewati direktori tidak relevan tanpa membuka file.
data/events/
├── date=2026-11-10/
│ ├── part-000.parquet
│ └── part-001.parquet
├── date=2026-11-11/
│ ├── part-000.parquet
│ └── part-001.parquet
└── date=2026-11-12/
└── part-000.parquet
Pada lab lokal yang kami pakai sebagai contoh, satu mesin virtual empat vCPU dengan RAM 8 GB menyimpan log akses web sintetis dalam struktur di atas. Datasetnya kecil, ditulis sekali per batch harian, lalu dibaca berulang oleh DuckDB dan pyarrow. Tidak ada cluster, object storage, maupun penulis bersamaan. Konfigurasi ini cukup untuk mempelajari perilaku format, tetapi tidak mewakili beban produksi.
Menulis Data Terpartisi
Penulisan dapat dilakukan langsung per file dengan pq.write_table, atau melalui API dataset yang mengurus partisi dan ukuran file. Untuk penulisan batch harian, pendekatan kedua lebih sedikit menimbulkan kesalahan penamaan direktori.
import pyarrow as pa
import pyarrow.dataset as ds
tabel = pa.table({
"ts": pa.array([], type=pa.timestamp("ms")),
"path": pa.array([], type=pa.string()),
"status": pa.array([], type=pa.int16()),
"latency_ms": pa.array([], type=pa.int32()),
})
partisi = ds.partitioning(
pa.schema([("date", pa.string())]),
flavor="hive",
)
ds.write_dataset(
tabel,
base_dir="data/events",
format="parquet",
partitioning=partisi,
max_rows_per_file=500_000,
file_options=ds.ParquetFileFormat().make_write_options(compression="zstd"),
)
Dua parameter yang layak diperhatikan adalah max_rows_per_file dan codec kompresi. Batas baris mencegah file raksasa yang sulit dibaca paralel, sementara codec menentukan trade-off waktu tulis, waktu baca, dan ukuran di disk. Zstandard umumnya memberi rasio lebih baik daripada Snappy dengan biaya CPU lebih tinggi; pilihannya bergantung pada pola akses dan anggaran mesin.
Apa yang Belum Kami Uji
Penting jujur tentang batas pengalaman ini. Kami belum menguji Parquet pada himpunan data multi-terabyte, belum membandingkan codec secara sistematis, dan belum menjalankan penulisan bersamaan dari beberapa proses ke direktori yang sama. Kami juga belum menguji pembacaan dari object storage kompatibel S3 seperti MinIO melalui jaringan, yang biasanya menambah latensi dan mengubah perilaku pembacaan rentang byte. Semua klaim performa di artikel ini kualitatif dan berasal dari sifat format, bukan pengukuran kami sendiri.
Catatan penting: Parquet bukan basis data. Ia tidak menyediakan transaksi, tidak mengunci penulisan, dan tidak mendukung pembaruan satu baris secara langsung. Jika kebutuhan Anda adalah update per baris, konsistensi bersamaan, atau latensi milidetik untuk aplikasi, gunakan basis data operasional. Parquet adalah lapisan penyimpanan untuk analitik, dan sering kali ia dipakai bersama format tabel atau katalog yang menambahkan jaminan tersebut di atasnya.
Membaca Data: Proyeksi Kolom dan Filter
Nilai Parquet paling terasa saat membaca. Dengan API dataset, pembaca dapat menyatakan kolom yang dibutuhkan dan filter yang berlaku, lalu membiarkan pustaka memanfaatkan partisi serta statistik row group. Kunci praktiknya sederhana: jangan pernah membaca seluruh tabel jika hanya butuh tiga kolom.
import pyarrow.dataset as ds
dataset = ds.dataset("data/events", format="parquet", partitioning="hive")
hasil = dataset.to_table(
columns=["ts", "status", "latency_ms"],
filter=(ds.field("date") == "2026-11-12") & (ds.field("status") >= 500),
)
Filter pada kolom partisi bekerja di tingkat direktori, sehingga file di tanggal lain tidak dibuka sama sekali. Filter pada kolom biasa bekerja melalui statistik row group bila tersedia. Mesin lain memiliki sintaks berbeda untuk tujuan yang sama; DuckDB, misalnya, dapat membaca glob file Parquet dan mengenali partisi Hive secara otomatis.
SELECT date_trunc('hour', ts) AS jam,
count(*) AS jumlah
FROM read_parquet('data/events/date=*/part-*.parquet',
hive_partitioning = true)
WHERE status >= 500
GROUP BY 1
ORDER BY 1;
Pola ini nyaman untuk eksplorasi lokal karena tidak memerlukan server, tetapi juga menunjukkan keterbatasan: tanpa katalog, daftar file harus ditemukan lewat glob atau pemindaian direktori. Ketika file mencapai puluhan ribu, operasi listing itu sendiri bisa menjadi biaya signifikan, terutama di object storage. Di sinilah katalog dan format tabel mulai dibutuhkan.
Jebakan yang Sering Muncul dan Batas Penggunaan
Masalah pertama adalah small files. Satu file kecil per menit terdengar rapi, tetapi menghasilkan ribuan file dengan overhead metadata dan pembukaan file. Pembacaan melambat bukan karena datanya besar, melainkan karena jumlah file terlalu banyak. Solusinya memadatkan file secara berkala (compaction) dan memilih ukuran sesuai pola baca.
Masalah kedua adalah partisi yang terlalu halus. Partisi per menit pada dataset yang tumbuh perlahan menghasilkan banyak direktori berisi file sangat kecil. Partisi sebaiknya mengikuti kolom yang benar-benar dipakai sebagai filter dan memiliki kardinalitas wajar, bukan semua kolom yang terlihat menarik.
Masalah ketiga berkaitan dengan kompatibilitas tipe. Menyimpan angka sebagai string, atau timestamp sebagai teks tanpa zona waktu, akan menghantui pemakaian di hilir. Lebih baik menetapkan skema secara eksplisit saat menulis daripada membiarkan inferensi menebak, karena inferensi bisa berbeda antar batch dan menghasilkan file dengan tipe yang tidak seragam dalam satu direktori.
Masalah keempat adalah menganggap file Parquet bisa diperbarui. Karena immutable, upsert berarti menulis ulang file terdampak. Tanpa format tabel, penulisan ulang menuntut koordinasi manual dan mudah menghasilkan data ganda bila proses gagal di tengah jalan.
Kapan Parquet Bukan Pilihan Tepat
Parquet kurang cocok untuk beban transaksional, pembaruan baris segera, atau pipeline berlaten sangat rendah per event. Untuk kasus itu, basis data operasional atau sistem streaming lebih tepat; Parquet biasanya muncul di ujung pipeline sebagai lapisan arsip analitik.
Ia juga bukan pilihan pertama untuk dataset sangat kecil yang sering diubah manual, karena file biner kurang nyaman diedit. Untuk pertukaran data sederhana dengan pihak tanpa tooling data, CSV masih lebih praktis meski kurang efisien.
Jika kebutuhan Anda adalah satu tabel besar yang diubah terus-menerus oleh banyak penulis, pertimbangkan format tabel di atas Parquet, atau mesin yang memang mengelola penyimpanan kolumnar secara internal. Dokumentasi Arrow untuk Parquet di arrow.apache.org/docs/python/parquet.html berguna untuk memahami opsi penulisan dan pembacaan pada tingkat pustaka, sementara detail format biner dijelaskan di halaman file format.
Kesimpulan dan Langkah Awal yang Wajar
Apache Parquet memberi tiga manfaat yang saling menguatkan: skema eksplisit, penyimpanan kolumnar dengan encoding dan kompresi, serta metadata untuk pembacaan selektif. Bagi tim yang mulai beralih dari CSV, langkah paling wajar adalah memilih satu dataset analitik yang sering dibaca, menuliskannya sebagai Parquet dengan skema eksplisit, lalu membandingkan kemudahan dan kecepatan query pada mesin yang sama.
Dari sana, pertanyaan berikutnya muncul alami: bagaimana file dipartisi, seberapa besar row group yang sesuai, codec apa yang dipakai, dan apakah dibutuhkan katalog atau format tabel. Semua keputusan itu sebaiknya diambil dari pengukuran pada data sendiri, bukan klaim umum. Parquet adalah fondasi yang kuat, tetapi tetap satu lapisan; nilai akhirnya ditentukan disiplin dalam menyusun skema, partisi, dan proses penulisan di sekitarnya.
COMMENTS