Penulisan terpartisi menyusun keluaran ke dalam folder menurut nilai satu atau beberapa kolom, sehingga tata letak folder menjadi indeks yang dimanfaatkan pembacaan. Topik ini membahas cara menulis dengan partitionBy, bentuk pohon folder yang dihasilkan, dan pemangkasan partisi yang terjadi ketika pembacaan menyaring kolom partisinya. Setelah membacanya, kolom partisi dapat dipilih dengan sadar, dan bukti pemangkasannya dapat ditunjukkan dari rencana eksekusi.
Kolom partisi dan pohon folder#
Metode partitionBy("kolom") dipanggil pada penulis sebelum format, dan Spark menyimpan setiap kelompok baris berdasarkan nilai kolomnya ke folder yang dinamai kolom=nilai. Kolom yang dipilih untuk partisi idealnya berisi nilai yang jumlahnya terbatas dan sering menjadi dasar penyaringan, misalnya kelas, tanggal, atau wilayah, karena setiap nilai uniknya menjadi satu folder yang berdiri sendiri.
Skrip berikut menulis data siswa dengan partisi atas kolom kelas, lalu menelusuri pohon folder keluarannya. Pohon itu diperiksa melalui pustaka baku Python, dan setiap folder partisi dilaporkan bersama jumlah berkas Parquet di dalamnya. Skrip ini menumpuk beberapa pekerjaan tulis dan telusur sehingga durasinya dekat batas verifikasi otomatis dan bervariasi antar percobaan, sehingga keluarannya diambil dari pengujian manual yang skripnya identik.
# tidak-dijalankan
import os
from pyspark.sql import SparkSession
from pyspark.sql import functions as F
spark = (
SparkSession.builder
.master("local[2]")
.appName("tulis-terpartisi")
.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",
)
target = "/tmp/lokasi/pq_terpartisi"
df.write.mode("overwrite").partitionBy("kelas").parquet(target)
print("=== pohon folder keluaran ===")
for root, dirs, files in sorted(os.walk(target)):
dirs.sort()
if files:
pq = [f for f in files if f.endswith(".parquet")]
print(root.replace(target, "."), "->", len(pq), "berkas parquet,", len(files) - len(pq), "berkas lain")
print("=== baca dengan filter partisi ===")
hasil = spark.read.parquet(target).where(F.col("kelas") == "10A")
hasil.explain()
hasil.orderBy("id").show()
spark.stop()WARNING: Using incubator modules: jdk.incubator.vector
=== pohon folder keluaran ===
. -> 0 berkas parquet, 2 berkas lain
./kelas=10A -> 1 berkas parquet, 1 berkas lain
./kelas=10B -> 1 berkas parquet, 1 berkas lain
=== baca dengan filter partisi ===
== Physical Plan ==
*(1) ColumnarToRow
+- FileScan parquet [id#5L,nama#6,nilai#7L,kelas#8] Batched: true, DataFilters: [], Format: Parquet, Location: InMemoryFileIndex(1 paths)[file:/tmp/lokasi/pq_terpartisi], PartitionFilters: [isnotnull(kelas#8), (kelas#8 = 10A)], PushedFilters: [], ReadSchema: struct<id:bigint,nama:string,nilai:bigint>
+---+----+-----+-----+
| id|nama|nilai|kelas|
+---+----+-----+-----+
| 1| Ali| 85| 10A|
| 2|Budi| 90| 10A|
+---+----+-----+-----+Keluaran menunjukkan struktur kelas=10A dan kelas=10B sebagai dua folder terpisah, dan berkas _SUCCESS terhitung sebagai berkas lain di setiap tingkat. Kolom kelas tidak lagi tersimpan di dalam berkas Parquet itu, melainkan hidup pada nama folder, dan hasil show() tetap menampilkannya sebagai kolom biasa karena pembaca mengembalikannya dari tata letak folder.
Pemangkasan partisi pada pembacaan#
Bagian PartitionFilters pada rencana memuat syarat (kelas#8 = 10A), dan pada saat yang sama ReadSchema hanya berisi id, nama, dan nilai, tanpa kelas. Kedua tanda itu menyatakan bahwa pembaca hanya menyentuh folder kelas=10A, membiarkan folder lain tidak dibuka sama sekali, lalu membaca tiga kolom yang tersimpan dari folder itu saja. Pada data nyata dengan ribuan folder tanggal, pemangkasan semacam ini adalah selisih antara membaca satu hari dan membaca seluruh sejarah tabel.
Efek ini menjelaskan mengapa pemilihan kolom partisi menentukan kinerja jangka panjang tabel. Kolom yang hampir selalu hadir sebagai syarat pembacaan, misalnya tanggal pada data peristiwa, layak menjadi kolom partisi. Sebaliknya, kolom dengan nilai yang nyaris unik seperti id menghasilkan folder per baris yang membanjiri sistem berkas, dan kolom yang jarang dijadikan syarat hanya menambah tingkat folder tanpa manfaat pembacaan.
Jumlah folder dan ukuran berkasnya#
Setiap folder partisi berisi berkas partisi dari task penulis, dan jumlahnya mengikuti jumlah partisi eksekusi saat menulis. Menulis dengan partisi eksekusi yang banyak pada folder yang banyak menghasilkan berkas kecil yang membanjiri sistem berkas, tema yang dibahas terpisah pada topik masalah berkas kecil. Karena itu keputusan kolom partisi dan keputusan jumlah partisi eksekusi ditimbang bersama, bukan terpisah.
Kolom partisi juga dapat berlapis, misalnya partitionBy("tahun", "bulan", "hari"), yang menghasilkan folder bertingkat dan pemangkasan bertahap dari tahun sampai hari. Semakin dalam tingkatannya, semakin banyak syarat yang diperlukan agar pemangkasan bekerja maksimal, sehingga kedalaman ini sebaiknya mengikuti pola kueri yang nyata, bukan asumsi semata.
Pemartisian juga dapat dibentuk bertingkat dengan memanggil partitionBy dengan beberapa kolom, misalnya partitionBy("kelas", "hari"), yang menghasilkan folder kelas=nilai berisi folder hari=nilai. Struktur bertingkat menjawab pola penyaringan yang berlapis, dan setiap tingkat menambah satu dimensi yang dipotong saat dibaca. Biayanya muncul pada jumlah folder yang tumbuh sebagai hasil kali nilai kedua kolom, sehingga pemilihan tingkat kedua mengikuti kebutuhan penyaringan yang benar-benar terjadi, bukan kemungkinan yang belum pernah dipakai. Pada data kecil, satu tingkat pemartisi hampir selalu cukup, dan struktur bertingkat baru menemukan manfaatnya ketika volume dan pola kueri sudah berbentuk nyata.
Kesalahan umum#
Kesalahan pertama adalah mempartisi pada kolom dengan kardinalitas tinggi, misalnya id atau stempel waktu mentah. Jumlah foldernya bertumbuh mengikuti jumlah nilai unik, sistem berkas dipenuhi berkas kecil, dan pemangkasan yang dijanjikan justru mengubah pembacaan menjadi penelusuran panjang. Turunkan kolomnya, misalnya menjadi tanggal tanpa jam, sebelum dijadikan partisi.
Kesalahan kedua adalah menyaring kolom partisi dengan ekspresi yang tidak dapat dipakai pemangkasan partisi, misalnya fungsi yang diterapkan pada kolom tanggalnya. Syarat yang tertekan harus berbentuk perbandingan langsung terhadap kolom partisi, dan pembacaan rencana dengan bagian PartitionFilters tetap menjadi bukti yang menentukan.
Kesalahan ketiga adalah menulis ulang satu partisi dengan mode("overwrite") tanpa opsi penggantian sebagian. Mode bawaan mengganti seluruh folder target sesuai lingkup penulisannya, dan perubahan sebagian perlu disusun dengan sadar, misalnya menulis hanya partisi yang datanya diperbarui. Kebiasaan memeriksa isi folder setelah penulisan, sebagaimana diperagakan skrip, mencegah kejutan hilangnya partisi lain.
Ringkasan#
partitionBy("kolom")menyusun keluaran ke folderkolom=nilai, dan kolom partisinya hidup pada nama folder, bukan di dalam berkas Parquet.- Pembacaan yang menyaring kolom partisi memicu pemangkasan yang terlihat pada
PartitionFilters, dan kolom partisi ikut hilang dariReadSchema. - Kolom partisi yang baik berkardinalitas terbatas dan sering menjadi syarat, sedangkan kolom nyaris unik menghasilkan banjir folder.
- Jumlah berkas per folder mengikuti partisi eksekusi saat menulis, sehingga keputusan partisi folder dan partisi eksekusi ditimbang bersama.
Referensi#
- Parquet Files: Partitioning — Apache Spark — Bagian dokumentasi resmi tentang penulisan terpartisi dan penemuan partisi saat membaca.
- Table Partitioning — Apache Spark — Penjelasan partisi pada panduan sumber data resmi.
- pyspark.sql.DataFrameWriter.partitionBy — Apache Spark — Referensi metode partitionBy pada penulis DataFrame.