Join menggabungkan baris dari dua DataFrame yang berpasangan pada satu atau beberapa kunci, sebagaimana gabungan tabel pada SQL. Topik ini membahas jenis join yang paling sering dipakai, cara menghadapi kolom kunci yang namanya sama di kedua sisi, dan pengenalan broadcast hint untuk tabel kecil. Setelah membacanya, gabungan data dapat dipilih jenisnya dengan sadar, kolom hasilnya tidak ambigu, dan pilihan strategi join untuk tabel kecil dapat dinyatakan secara eksplisit.
Jenis join dan isi barisnya#
Jenis join menentukan baris mana yang bertahan ketika pasangannya tidak lengkap. Inner join hanya menyimpan baris yang kuncinya ada di kedua sisi. Left join menyimpan seluruh baris sisi kiri, dan baris tanpa pasangan diisi NULL pada kolom sisi kanan. Right join adalah kebalikannya, sedangkan full outer join menyimpan baris kedua sisi sekaligus. Jenis lain seperti semi join dan anti join hanya menguji keberadaan pasangan tanpa menambah kolom, dan berguna untuk penyaringan berbasis tabel pembanding.
Pemilihan jenis join sebaiknya ditetapkan secara eksplisit dengan argumen how, bahkan ketika tujuannya inner join. Nilai default yang diam-diam dipakai membuat maksud program kurang terbaca, sedangkan how="inner" yang tertulis menjadikan baris kode itu menjelaskan maksudnya sendiri. Konvensi inilah yang dipakai pada seluruh contoh di jalur ini.
Inner, left, dan broadcast pada satu skrip#
Skrip berikut memasangkan data siswa dengan catatan kehadiran. DataFrame absen sengaja hanya memuat tiga siswa, sehingga satu siswa tidak memiliki pasangan dan perbedaan inner dengan left langsung terlihat.
from pyspark.sql import SparkSession
spark = (
SparkSession.builder
.master("local[2]")
.appName("uji-join")
.config("spark.sql.shuffle.partitions", "2")
.getOrCreate()
)
siswa = spark.createDataFrame(
[(1, "Ali", "10A"), (2, "Budi", "10A"), (3, "Citra", "10B"), (4, "Dewi", "10B")],
"id long, nama string, kelas string",
)
absen = spark.createDataFrame(
[(1, "hadir"), (2, "hadir"), (3, "izin")],
"id long, status_absen string",
)
print("=== inner join on id ===")
siswa.join(absen, on="id").orderBy("id").show()
print("=== left join: siswa tanpa catatan absen ===")
siswa.join(absen, on="id", how="left").orderBy("id").show()
spark.stop()WARNING: Using incubator modules: jdk.incubator.vector
=== inner join on id ===
+---+-----+-----+------------+
| id| nama|kelas|status_absen|
+---+-----+-----+------------+
| 1| Ali| 10A| hadir|
| 2| Budi| 10A| hadir|
| 3|Citra| 10B| izin|
+---+-----+-----+------------+
=== left join: siswa tanpa catatan absen ===
+---+-----+-----+------------+
| id| nama|kelas|status_absen|
+---+-----+-----+------------+
| 1| Ali| 10A| hadir|
| 2| Budi| 10A| hadir|
| 3|Citra| 10B| izin|
| 4| Dewi| 10B| NULL|
+---+-----+-----+------------+Perbandingan kedua join memperlihatkan satu baris selisih. Inner join membuang Dewi yang tidak memiliki catatan absen, sedangkan left join mempertahankannya dengan NULL pada status_absen. Ketika argumen on="id" diberikan sebagai nama kolom yang ada di kedua sisi, Spark menggabungkan kunci tersebut menjadi satu kolom hasil, sehingga tabel keluaran hanya memuat satu kolom id.
Kunci duplikat dan pilihan kolom eksplisit#
Masalah kolom kunci muncul ketika join ditulis sebagai ekspresi perbandingan, misalnya siswa.join(label, siswa["id"] == label["id"]). Pada bentuk ini kedua kolom id ikut masuk ke hasil, sehingga rujukan siswa["id"] menjadi ambigu dan error analisis muncul pada transformasi berikutnya. Solusinya adalah merujuk kolom beserta DataFrame asalnya saat menyusun select berikutnya.
# tidak-dijalankan
label = spark.createDataFrame(
[(1, "Ali", "kelas-10A"), (2, "Budi", "kelas-10A")],
"id long, nama string, kelompok string",
)
gabungan = siswa.join(label, siswa["id"] == label["id"])
gabungan.select(siswa["id"], siswa["nama"], label["kelompok"]).show()Rujukan bergaya siswa["id"] dan label["kelompok"] menyatakan sisi asal setiap kolom secara tegas. Apabila nama kolom memang identik dan isi kuncinya juga identik, bentuk on="id" lebih disukai karena otomatis menggabungkan kuncinya. Kedua gaya ini melengkapi satu sama lain, yaitu on="nama-kolom" untuk kunci yang bersih, dan ekspresi perbandingan untuk kunci dengan nama atau syarat yang berbeda.
Broadcast hint: tabel kecil dikirim ke setiap pekerja#
Satu perilaku kunci join yang sering terlupa perlu diketahui sejak awal, yaitu kunci yang bernilai null. Kecocokan join membandingkan nilai kunci, dan null tidak sama dengan null menurut semantik SQL, sehingga baris dengan kunci null pada kedua sisi tidak pernah berpasangan dan nasibnya mengikuti jenis join yang dipilih. Data yang kuncinya belum dibersihkan karenanya perlu diperiksa dengan F.isNull() sebelum join, sebab baris yang tampak seharusnya cocok justru terpisah tanpa pesan apa pun.
Secara default, Spark memilih strategi join berdasarkan ukuran estimasi kedua sisi. Apabila satu sisi cukup kecil untuk muat di memori setiap executor, seluruh isinya dikirim ke setiap executor, lalu setiap partisi sisi besar mencocokkan dirinya secara lokal tanpa mengirim data besar melalui jaringan. Strategi ini disebut broadcast hash join, dan pada data besar selisih waktunya besar karena sisi besar tidak perlu di-shuffle.
Fungsi F.broadcast() adalah hint yang menyatakan sisi mana yang ingin dikirim. Pada data contoh topik ini, menambahkan F.broadcast(absen) pada join tidak mengubah isi baris sama sekali, hasilnya identik dengan tabel inner join di atas, dan yang berbeda hanya rencana eksekusinya. Perbedaan rencana itu dapat diperiksa dengan explain() yang diperkenalkan pada modul cara Spark berjalan, dan modul esensi penyetelan memperlihatkan kedua rencana itu berdampingan. Hint ini aman dipakai ketika sisi yang dikirim memang kecil, dan ambang batas otomatisnya dibahas pada modul esensi penyetelan.
Kesalahan umum#
Kesalahan pertama adalah lupa argumen how ketika maksudnya bukan inner join, sehingga baris yang hilang tidak terdeteksi. Akibatnya data yang seharusnya tetap muncul justru dibuang secara senyap. Perbaikannya adalah selalu menulis how secara eksplisit dan memeriksa jumlah baris sebelum dan sesudah join pada data yang belum dikenal.
Kesalahan kedua adalah merujuk kolom kunci tanpa menyebut asalnya setelah join dengan ekspresi perbandingan. Rujukan ambigu memunculkan error AnalysisException yang menyebut referensi id bersifat ambigu. Perbaikannya adalah memberi awalan DataFrame pada select berikutnya, atau mengubah join menjadi bentuk on="id" yang menggabungkan kunci.
Kesalahan ketiga adalah mem-broadcast tabel yang sebenarnya besar. Seluruh isinya dikirim ke setiap executor dan disalin di memori masing-masing, sehingga memori habis atau pekerjaan menjadi lebih lambat daripada join biasa. Hint hanya untuk sisi yang memang kecil ukurannya, dan sisa kasus sebaiknya dibiarkan pada estimasi otomatis Spark.
Ringkasan#
- Jenis join menentukan nasib baris tanpa pasangan, yaitu inner membuangnya, left dan right mempertahankan satu sisi dengan
NULL, dan full outer mempertahankan kedua sisi. - Argumen
on="nama-kolom"menggabungkan kolom kunci yang namanya sama, sedangkan join dengan ekspresi perbandingan membiarkan kedua kolom kunci dan menuntut rujukan eksplisit padaselectberikutnya. F.broadcast(df)menyatakan sisi kecil yang ingin dikirim ke setiap executor sehingga sisi besar tidak perlu di-shuffle.- Kebiasaan menulis
howsecara eksplisit membuat maksud join terbaca dan mencegah baris hilang secara senyap.
Referensi#
- pyspark.sql.DataFrame.join — Apache Spark — Referensi lengkap parameter on dan how beserta jenis join yang didukung.
- pyspark.sql.functions.broadcast — Apache Spark — Dokumentasi broadcast hint pada fungsi kolom.
- Join-strategy hints — Apache Spark — Bagian panduan penyetelan yang menjelaskan hint join termasuk broadcast.