This episode covers Spark's core analytical operations: the various join types from inner to anti, groupBy and advanced grouping aggregations, window functions for time-based analytics, and the performance implications of joins and shuffle on large queries.

Now that you've mastered DataFrames and basic SQL, episode 7 moves into the analytical operations most used in the real world: joins, aggregations, and window functions. These three are the everyday language of data analysts and data engineers.
In this episode we cover all the available join types, groupBy aggregations and their advanced variants, window functions for sequence-ordered analysis, and performance implications — because joins and shuffle are the two most common reasons queries slow down.
Spark supports all the standard SQL join types. The clearest example uses PySpark:
pelanggan = spark.createDataFrame([(1, "budi"), (2, "sari")], ["id", "nama"])
pesanan = spark.createDataFrame([(1, "p1"), (1, "p2"), (3, "p3")], ["id", "produk"])
inner = pelanggan.join(pesanan, "id", "inner")
inner.show()pelanggan.join(pesanan, "id", "inner") only returns the rows whose id matches on both sides. The available join types:
left semi → "data that has a match" (filter)
left anti → "data that has no match" (data quality check)left_anti, for example, is very practical for finding orders whose customer id doesn't exist in the customer table.
A cross join produces the product of all rows from both sides. Because the result can explode exponentially, Spark requires explicit confirmation before running it.
groupBy groups rows by a key and then applies an aggregation function:
from pyspark.sql import functions as F
penjualan = spark.read.option("header", True).csv("data/penjualan.csv")
hasil = penjualan \
.groupBy("kota", "kategori") \
.agg(
F.sum("jumlah").alias("total"),
F.avg("harga").alias("harga_rata"),
F.countDistinct("produk").alias("produk_unik"),
)
hasil.show()F.sum, F.avg, and F.countDistinct are built-in aggregation functions. groupBy can take multiple columns for deeper granularity — city per category analysis, for example.
For multidimensional analysis, Spark provides three variants:
rollup(a, b) → (a,b), (a), (total)
cube(a, b) → (a,b), (a), (b), (total)cube produces more rows because it covers all combinations. This is useful for reports that need to view data from various angles without writing several separate queries.
For aggregations that move over an ordered window (for example the average of the last 7 days), combine groupBy with the window functions in the next section.
Unlike groupBy, which collapses rows, a window function computes aggregate values while keeping every row. Define the window with partitionBy and orderBy:
from pyspark.sql.window import Window
w = Window.partitionBy("kota").orderBy(F.col("total").desc())
hasil = penjualan.withColumn("peringkat", F.row_number().over(w))
hasil.filter(F.col("peringkat") == 1).show()F.row_number().over(w) assigns a sequential number within each city partition ordered by the largest total. The result: the top 1 sale per city — without losing any other columns.
The most frequently used functions:
w2 = Window.partitionBy("kota").orderBy("bulan")
hasil2 = penjualan.withColumn("bulan_lalu", F.lag("total").over(w2)) \
.withColumn("delta", F.col("total") - F.col("bulan_lalu"))F.lag("total").over(w2) fetches the total value from the previous month within the same city — the basic pattern for time-series analysis like growth calculations.
For aggregations over a time range (for example per hour), use the F.window function:
from pyspark.sql import functions as F
event = spark.read.json("data/event.json")
per_jam = event \
.groupBy(F.window("waktu", "1 hour"), "produk") \
.count()
per_jam.show()F.window("waktu", "1 hour") groups time into hourly windows — a pattern that will reappear in Structured Streaming in episode 10.
Most joins require shuffle: data is regrouped into partitions by the join key and sent between executors over the network. The larger the data, the larger the cost. There are several strategies to reduce this cost:
broadcast (small side) → bucket join (prepared) → sort-merge (default)The strategies and when to use them will be covered fully in episode 9. What you should remember now: every join and groupBy that triggers a shuffle is a prime optimization candidate.
Warning
Watch out for data skew: if one join key dominates (for example one city holds 90 percent of transactions), the executor holding that key becomes a bottleneck. This is a common problem that requires the special solutions covered in episode 9.
Episode 7 equips you with the three core analytical operations: joins with eight types from inner to anti, groupBy aggregations with rollup and cube, and window functions for sequential and time-based analysis. You also understand why joins and shuffle are the main sources of cost.
Key takeaways:
left_anti is a tool for detecting data missing from a reference table.In the next episode, episode 8, we'll discuss data sources and storage — connecting Spark to HDFS, S3, JDBC, and the file system, reading and writing Parquet, Avro, ORC, JSON, and CSV, and strategies for partitioning, bucketing, file layout, and schema evolution.