The heart of every report: collapse many rows into one number per group. groupBy + agg, the full family of aggregate functions, collect_list, pivot, and rollup/cube for subtotals. The core functions run live below.
The dataset
This module uses the two DataFrames from Foundations — we mostly aggregate over orders:
Aggregation is always two moves: group the rows by one or more columns, then reduce each group to a summary. In Spark that's groupBy(...) followed by agg(...) (or a shortcut like .count()). Every row in a group is folded into a single output row.
Run the equivalent Spark SQL — edit it and press Run
SQL
⌘/Ctrl + Enter
Any column you select alongside an aggregate must either be in the groupBy or wrapped in its own aggregate — otherwise Spark can't decide which row's value to show. Prefer agg with named aliases over the bare shortcuts (.count(), .sum()) once you have more than one measure; it keeps column names explicit and readable.
6.2
sum, avg, min, max & count
The workhorses. Inside one agg you can compute as many measures as you like — each becomes a column. This is how you turn a raw event table into a revenue summary.
Signature
def sum(e: Column): Column def avg(e: Column): Column def mean(e: Column): Column
def min(e: Column): Column def max(e: Column): Column def count(e: Column): Column
// all live in org.apache.spark.sql.functions
Run the equivalent Spark SQL — edit it and press Run
SQL
⌘/Ctrl + Enter
count(*) counts rows; count(col) counts non-null values of that column — a handy way to measure completeness. sum and avg silently ignore nulls too, so a column that's half-null still averages only the present values. If that's not what you want, coalesce the nulls to 0 first.
6.3
countDistinct & approx_count_distinct
Counting unique values — how many distinct customers, products, or sessions. countDistinct is exact; approx_count_distinct trades a little accuracy (HyperLogLog) for big speed on huge cardinalities.
Run the equivalent Spark SQL — edit it and press Run
SQL
⌘/Ctrl + Enter
countDistinct requires a shuffle and can be expensive on billions of rows. For dashboards where being within ~2% is fine, approx_count_distinct($"user_id", 0.02) is dramatically cheaper and its memory footprint is fixed regardless of cardinality. Never wrap countDistinct in another aggregate — combine multiple distinct counts in one agg instead.
6.4
collect_list & collect_set
Sometimes you don't want a number — you want the actual values rolled into an array. collect_list gathers every value in the group (with duplicates, unordered); collect_set gathers the distinct values. This is the standard way to denormalise "all products a customer bought" into one row.
Signature
def collect_list(e: Column): Column // Array, keeps duplicates, no ordering guarantee
def collect_set(e: Column): Column // Array of distinct values
baked output — Spark-only type, not runnable in the browser engine
The result is an Array column you can later explode back out (see the Arrays module) or feed to higher-order functions. Order is not guaranteed — if you need the array sorted by, say, order date, use sort_array afterwards or collect over a window with an ordered frame. collect_set on very high-cardinality groups can blow up memory since the whole array lives on one executor; aggregate to a count first when you only need the size.
6.5
pivot — rows into columns
pivot rotates distinct values of one column into new columns, filling each cell with an aggregate. It's how you build a cross-tab: category down the side, order status across the top, revenue in the cells.
Signature
def pivot(pivotColumn: String): RelationalGroupedDataset
def pivot(pivotColumn: String, values: Seq[Any]): RelationalGroupedDataset // faster: values known
// used as: df.groupBy(...).pivot("col").agg(...)
pivot("status").agg(sum(amount)) — one column per status
category
cancelled
delivered
pending
shipped
Electronics
null
105.0
null
9.0
Home
12.0
null
32.0
null
Stationery
null
null
null
6.5
baked output — Spark-only type, not runnable in the browser engine
The browser engine can't run pivot, but you can hand-roll the same cross-tab with CASE WHEN — which is exactly what Spark compiles a pivot down to. Run it:
Scala
// the CASE-WHEN form Spark generates under the hood
selectExpr(...) // conceptually
Run the equivalent Spark SQL — edit it and press Run
SQL
⌘/Ctrl + Enter
Always pass the explicit value list — pivot("status", Seq("delivered","shipped","pending","cancelled")) — in production. Without it Spark runs an extra job just to discover the distinct values, and an unbounded pivot column can explode into thousands of columns and OOM the driver.
6.6
rollup, cube & statistical aggregates
rollup and cube add subtotal and grand-total rows to a grouped result. rollup(a, b) gives totals for each a plus one overall total; cube(a, b) gives every combination. The extra rows carry null in the grouped-out columns.
Signature
def rollup(cols: Column*): RelationalGroupedDataset // hierarchical subtotals + grand total
def cube(cols: Column*): RelationalGroupedDataset // all combinations of subtotals
def stddev(e: Column): Column def variance(e: Column): Column def corr(a: Column, b: Column): Column
stddev/variance of amount across all 6 orders (sample stddev)
sd
var
28.16
793.07
baked output — Spark-only type, not runnable in the browser engine
Use grouping_id() alongside rollup/cube to tell a real null apart from a "this column was totalled out" null — otherwise your subtotal rows are indistinguishable from missing data. stddev defaults to the sample standard deviation (n−1); use stddev_pop for the population version.
P
PriyaFounder · Kettle & Co.now
Board deck tonight 📊 — I need total revenue per category, biggest first. Column names: category and revenue.
🎯 Your mission From orders, return each category with its total sum(amount) as revenue, sorted highest revenue first.
SQL
⌘/Ctrl + Enter
In Spark this is orders.groupBy("category").agg(sum("amount").as("revenue")).orderBy($"revenue".desc).
SELECT category, sum(amount) AS revenue FROM orders GROUP BY category ORDER BY revenue DESC;
Frequently asked questions
What is the difference between groupBy and agg in Spark?
groupBy defines the grouping keys and returns a RelationalGroupedDataset; it does not compute anything on its own. agg is what you call on that grouped dataset to specify the aggregate expressions (sum, avg, count…). You almost always chain them: df.groupBy("category").agg(sum("amount")).
How do I do a pivot table in Spark?
Chain groupBy(rowKey).pivot("columnKey").agg(aggFn). For example orders.groupBy("category").pivot("status").agg(sum("amount")) turns each distinct status into its own column. Always pass the explicit value list — pivot("status", Seq("delivered","shipped")) — in production so Spark doesn't run an extra scan to discover the values.
What is the difference between collect_list and collect_set in Spark?
Both roll a group's values into an array. collect_list keeps every value including duplicates; collect_set keeps only the distinct values. Neither guarantees ordering, so sort the array afterwards with sort_array or collect over an ordered window if order matters.
How do I count distinct values in Spark?
Use countDistinct(col) for an exact count, or approx_count_distinct(col, rsd) for a much faster HyperLogLog estimate within a target relative error. On very large datasets the approximate version has a fixed memory footprint and avoids the expensive exact-distinct shuffle.
Ready for the full data stack?
These Spark functions are one piece. When you want pipelines, Kafka, lakehouse table formats, and a full end-to-end project, the Data Engineering course is free too.