Free Interactive Course · Spark + Scala

Aggregations & Grouping

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:

peopleid, name, country, age, signup_date
ordersid, user_id, product, category, amount, qty, status, ordered_at
6.1

groupBy & agg — the pattern

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.

Signature
def groupBy(cols: Column*): RelationalGroupedDataset
def agg(expr: Column, exprs: Column*): DataFrame
// shortcuts on the grouped dataset:
def count(): DataFrame     def sum(col: String*): DataFrame     def avg(col: String*): DataFrame
Scala
import org.apache.spark.sql.functions.count

orders
  .groupBy($"category")
  .agg(count("*").as("n"))
  .orderBy($"category")
  .show(false)
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
Scala
import org.apache.spark.sql.functions.{sum, avg, min, max, round}

orders.groupBy($"category").agg(
  sum($"amount").as("revenue"),
  round(avg($"amount"), 2).as("avg_amount"),
  min($"amount").as("lo"),
  max($"amount").as("hi")
).orderBy($"revenue".desc).show(false)
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.

Signature
def countDistinct(expr: Column, exprs: Column*): Column
def approx_count_distinct(e: Column): Column
def approx_count_distinct(e: Column, rsd: Double): Column   // rsd = max relative error
Scala
import org.apache.spark.sql.functions.countDistinct

orders.agg(
  count("*").as("rows"),
  countDistinct($"user_id").as("buyers"),
  countDistinct($"category").as("categories")
).show(false)
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
Scala
import org.apache.spark.sql.functions.collect_list

orders.groupBy($"category")
  .agg(collect_list($"product").as("products"))
  .orderBy($"category").show(false)
collect_list returns a real Array column — shown baked (the browser SQL engine has no array type)
categoryproducts
Electronics[Wireless Mouse, Mechanical Keyboard, USB-C Cable]
Home[Desk Lamp, Coffee Mug]
Stationery[Notebook]
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(...)
Scala
orders.groupBy($"category")
  .pivot("status")
  .agg(sum($"amount"))
  .orderBy($"category").show(false)
pivot("status").agg(sum(amount)) — one column per status
categorycancelleddeliveredpendingshipped
Electronicsnull105.0null9.0
Home12.0null32.0null
Stationerynullnullnull6.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
Scala
orders.rollup($"category")
  .agg(sum($"amount").as("revenue"))
  .orderBy($"category".asc_nulls_last).show(false)
rollup adds the grand-total row (category = null → 164.5)
categoryrevenue
Electronics114.0
Home44.0
Stationery6.5
null164.5
baked output — Spark-only type, not runnable in the browser engine

The statistical aggregates — stddev, variance, skewness, corr, covar_pop — summarise the shape of a numeric column, not just its total:

Scala
import org.apache.spark.sql.functions.{stddev, variance}

orders.agg(
  round(stddev($"amount"), 2).as("sd"),
  round(variance($"amount"), 2).as("var")
).show(false)
stddev/variance of amount across all 6 orders (sample stddev)
sdvar
28.16793.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

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.

Explore Data Engineering →

Comments

0

Join the conversation. Sign in to leave a comment — questions and feedback welcome.