Pertanyaan yang hampir selalu muncul ketika dua antarmuka tersedia adalah mana yang lebih baik. Spark menjawabnya dengan fakta arsitektur, yaitu SQL dan API DataFrame hanyalah dua bahasa depan yang berujung pada mesin eksekusi yang sama. Kueri yang setara akan disusun menjadi rencana logis yang sama, dioptimalkan oleh optimizer yang sama, dan dieksekusi dengan biaya yang sama. Topik ini membuktikan paritas itu langsung dengan explain(), lalu membahas cara memilih antarmuka mengikuti tugas.
Satu kueri, dua penulisan#
Skrip berikut menghitung rata-rata nilai per kelas dua kali dengan data yang sama. Versi SQL menuliskannya sebagai pernyataan tunggal, sedangkan versi DataFrame merangkai groupBy, agg, dan orderBy. Setelah keduanya ditampilkan, skrip membandingkan rencana fisik satu kueri penyaringan yang ditulis dengan kedua gaya.
from pyspark.sql import SparkSession
from pyspark.sql import functions as F
spark = (
SparkSession.builder
.master("local[2]")
.appName("dua-api")
.config("spark.sql.shuffle.partitions", "2")
.getOrCreate()
)
df = spark.read.option("header", "true").csv("nilai_ujian.csv")
df.createOrReplaceTempView("nilai_ujian")
print("=== versi SQL ===")
spark.sql("""
SELECT kelas, avg(nilai) AS rata_rata
FROM nilai_ujian
GROUP BY kelas
ORDER BY kelas
""").show()
print("=== versi DataFrame ===")
df.groupBy("kelas").agg(F.avg("nilai").alias("rata_rata")).orderBy("kelas").show()
print("=== rencana keduanya identik? ===")
print(spark.sql("SELECT kelas FROM nilai_ujian WHERE nilai >= 88").explain() is None)
df.filter(F.col("nilai") >= 88).select("kelas").explain()
spark.stop()WARNING: Using incubator modules: jdk.incubator.vector
=== versi SQL ===
+-----+-----------------+
|kelas| rata_rata|
+-----+-----------------+
| 10A|87.66666666666667|
| 10B|88.33333333333333|
+-----+-----------------+
=== versi DataFrame ===
+-----+-----------------+
|kelas| rata_rata|
+-----+-----------------+
| 10A|87.66666666666667|
| 10B|88.33333333333333|
+-----+-----------------+
=== rencana keduanya identik? ===
== Physical Plan ==
*(1) Project [kelas#19]
+- *(1) Filter (isnotnull(nilai#20) AND (cast(nilai#20 as bigint) >= 88))
+- FileScan csv [kelas#19,nilai#20] Batched: false, DataFilters: [isnotnull(nilai#20), (cast(nilai#20 as bigint) >= 88)], Format: CSV, Location: InMemoryFileIndex(1 paths)[file:/home/kal/my-project/content/spark/sql-di-spark/paritas-api/nilai_u..., PartitionFilters: [], PushedFilters: [IsNotNull(nilai)], ReadSchema: struct<kelas:string,nilai:string>
True
== Physical Plan ==
*(1) Project [kelas#19]
+- *(1) Filter (isnotnull(nilai#20) AND (cast(nilai#20 as bigint) >= 88))
+- FileScan csv [kelas#19,nilai#20] Batched: false, DataFilters: [isnotnull(nilai#20), (cast(nilai#20 as bigint) >= 88)], Format: CSV, Location: InMemoryFileIndex(1 paths)[file:/home/kal/my-project/content/spark/sql-di-spark/paritas-api/nilai_u..., PartitionFilters: [], PushedFilters: [IsNotNull(nilai)], ReadSchema: struct<kelas:string,nilai:string>Bukti paritas dari rencana eksekusi#
Dua tabel keluaran pertama menunjukkan hasil yang sama persis, baris demi baris dan angka demi angka. Bagian yang lebih menarik adalah dua rencana fisik di bawahnya. Rencana pertama berasal dari string SQL dan rencana kedua dari rantai DataFrame, tetapi isinya identik dari baris pertama sampai terakhir. Proyeksi, penyaringan, bahkan detail seperti PushedFilters dan ReadSchema pada pemindaian CSV semuanya sama.
Baris True yang muncul di antara kedua rencana perlu penjelasan singkat. explain() mencetak rencana ke konsol lalu mengembalikan None, sehingga memanggilnya di dalam print() menghasilkan rencana diikuti teks True karena None is None bernilai benar. Trik kecil ini sekaligus menjadi pengingat bahwa explain() bersifat lazy dan tidak menjalankan pekerjaan apa pun, sebagaimana dibahas pada modul cara Spark berjalan.
Kesimpulannya tegas, yaitu perdebatan mana yang lebih cepat antara SQL dan DataFrame tidak relevan di Spark karena keduanya berujung pada optimizer yang sama. Perbedaan kinerja yang pernah terasa pada Spark versi awal telah hilang sejak DataFrame berbasis optimizer diperkenalkan. Pilihan antarmuka karenanya menjadi soal keterbacaan dan konteks tim, bukan kecepatan.
Memilih antarmuka mengikuti tugas#
Karena kecepatannya setara, kriteria pemilihan bergeser ke kejelasan maksud. SQL menang pada logika yang lebih alami dinyatakan dalam himpunan dan tabel, misalnya gabungan berlapis, ekspresi tabel umum, dan fungsi jendela yang ditulis dengan klausa OVER. Analis yang terbiasa dengan basis data relasional bisa membaca dan menulis SQL tanpa belajar API Python, sehingga kueri tinjauan mudah ditinjau bersama.
API DataFrame menang pada logika yang bersifat programatik. Perulangan atas daftar kolom, penyusunan nama kolom secara dinamis, percabangan berdasarkan data, dan pemanggilan fungsi bantu Python semuanya sulit atau tidak mungkin ditulis dalam string SQL. Integrasi dengan kode produksi Python juga lebih mulus karena pengecekan nama fungsi terjadi saat penulisan, bukan saat eksekusi.
Praktik yang sehat di banyak tim adalah memakai keduanya tanpa dogma. Bagian pembacaan dan penggabungan data ditulis dalam SQL agar terbaca seperti laporan, lalu bagian pembersihan dan penulisan hasil dikerjakan dengan DataFrame. Karena paritas rencana sudah terbukti, refaktor dari satu gaya ke gaya lain tidak mengubah hasil maupun kinerja, sehingga pemilihan bisa ditunda sampai bentuk logikanya jelas.
Kebiasaan tim menulis SQL sering melampaui sekadar pemilihan antarmuka per kueri. Pernyataan SQL yang panjang dapat disimpan sebagai berkas tersendiri dalam repositori, dilengkapi komentar dan riwayat perubahan, lalu dibaca aplikasi Python saat berjalan dan dijalankan melalui spark.sql(). Pola ini memisahkan logika kueri dari kode penggeraknya, memudahkan analis mengubah kueri tanpa menyentuh aplikasi, dan pindahnya kueri dari string di dalam kode ke berkas terpisah tidak mengubah kinerja maupun hasilnya sama sekali.
Kesalahan umum#
Kesalahan pertama adalah menganggap satu antarmuka punya fitur yang tidak dimiliki yang lain. Hampir semua fungsi SQL punya padanan dalam pyspark.sql.functions, dan hampir semua transformasi DataFrame bisa ditulis ulang sebagai SQL. Apabila sebuah fitur nyaris tidak ditemukan, kemungkinan besar namanya berbeda antara kedua antarmuka, dan pencarian di dokumentasi dua kali lebih cepat daripada mengira fiturnya tidak ada.
Kesalahan kedua adalah menyisipkan variabel Python ke string SQL dengan penggabungan teks biasa. Pola "WHERE kelas = " + kelas rentan rusak ketika nilai berisi tanda kutip dan menyulitkan penggunaan ulang. Pola yang benar adalah parameterisasi dengan spark.sql(query, args={"kelas": kelas}) yang meneruskan nilai sebagai parameter bernama.
Kesalahan ketiga adalah membandingkan kinerja antarmuka dengan mengukur dua kueri yang tidak benar-benar setara, misalnya versi SQL yang menyaring lebih awal daripada versi DataFrame. Perbandingan hanya sah ketika rencana fisiknya identik, dan cara memastikannya sudah ditunjukkan di atas, yaitu membandingkan keluaran explain() keduanya.
Ringkasan#
- SQL dan API DataFrame disusun menjadi rencana yang sama dan dieksekusi oleh mesin yang sama, sehingga kueri yang setara berjalan dengan kinerja yang sama.
explain()membuktikan paritas tersebut, dan keluarannya identik untuk kedua penulisan kueri yang setara.- SQL dipilih ketika logika berpikir dalam tabel dan perlu dibaca analis, sedangkan DataFrame dipilih ketika logika bersifat programatik.
- Pencampuran kedua gaya dalam satu alur tidak menimbulkan biaya konversi karena keduanya bertemu di optimizer yang sama.
Referensi#
- pyspark.sql.DataFrame.explain — Apache Spark — Referensi mode-mode explain untuk membandingkan rencana logis dan fisik.
- Spark SQL guide — Apache Spark — Panduan yang menjelaskan kedudukan SQL dan DataFrame pada mesin yang sama.
- Spark SQL adaptive execution dan optimizer — Ringkasan lapisan optimasi yang bekerja di bawah kedua antarmuka.