max_by / min_by
Return the value from the row where another column is largest, or smallest, in each group. One aggregate instead of a window, a row number and a filter.
On this page
Show code in
Every code block on the page follows this.
You will learn
- How max_by picks a row inside each group
- Why it is cheaper than the window workaround
- How to return several columns from the winning row
- What happens with ties and nulls
Read first
- groupBy + agg · 3 min read
- row_number / rank · 3 min read
Comfortable with these? Read on.
max_by(x, ord) returns x from the row with the largest ord in each group; min_by uses the smallest. Available in Spark SQL since 3.0 and in the PySpark API since 3.3.What it does
max_by(page, ts) finds the row with the largest ts in each group and returns that row's page. min_by does the same for the smallest. It is often called argmax.
Step by step
pageviews
| session_id | page | ts |
|---|---|---|
| s1 | /home | 10:00 |
| s1 | /pricing | 10:02 |
| s1 | /checkout | 10:05 |
| s2 | /search | 11:00 |
| s2 | /product/42 | 11:01 |
max_by(page, ts)
| session_id | exit |
|---|---|
| s1 | /checkout |
| s2 | /product/42 |
- 1Spark splits the rows into groups by
session_id. - 2For each group it keeps one running pair: the largest
tsseen so far and thepagefrom that row. - 3Each new row either replaces the pair, if its
tsis larger, or is ignored. - 4When the group ends, Spark returns the
pagehalf of the pair. No sorting happens at any point.
Run the example
from pyspark.sql import functions as F result = pageviews.groupBy("session_id").agg( F.min_by("page", "ts").alias("entry"), F.max_by("page", "ts").alias("exit"), )
SELECT session_id, min_by(page, ts) AS entry, max_by(page, ts) AS exit FROM pageviews GROUP BY session_id
Switch to PySpark to edit and run this example in your browser.
Without it
The usual workaround ranks rows in a window and keeps the first one: three steps and a sort for a single value.
from pyspark.sql import functions as F from pyspark.sql.window import Window w = Window.partitionBy("session_id").orderBy(F.desc("ts")) (pageviews.withColumn("pos", F.row_number().over(w)) .filter("pos = 1").select("session_id", "page"))
SELECT session_id, page FROM ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY session_id ORDER BY ts DESC) AS rn FROM pageviews) WHERE rn = 1
Performance
| max_by | row_number window | |
|---|---|---|
| Before the shuffle | Partial aggregation: one row per group per partition | Nothing: every row is shuffled |
| Sorting | None | Sorts every row by group and time |
| Physical plan | HashAggregate → Exchange → HashAggregate | Exchange → Sort → Window → Filter |
With many rows per group, max_by moves far less data across the network. The window version is still right when you need the top N rows, or the whole winning row with many columns and a defined tie rule.
Several columns from the winning row
Calling max_by once per column is fine when the ordering column is unique. With ties, each call can pick a different row. Pack the columns into one struct so they always come from the same row:
pageviews.groupBy("session_id").agg( F.max_by(F.struct("page", "referrer"), "ts").alias("last") ).select("session_id", "last.page", "last.referrer")
SELECT session_id, last.page, last.referrer FROM ( SELECT session_id, max_by(struct(page, referrer), ts) AS last FROM pageviews GROUP BY session_id)
An older trick does the same without max_by: max(struct(ts, page)). Structs compare field by field, so the maximum struct is the one with the latest ts, and you read .page from it.
Common mistakes
Ignoring ties
Expecting null ordering values to count
Using F.max_by on PySpark before 3.3
F.expr("max_by(page, ts)"); the SQL function exists since 3.0.Key takeaways
- max_by(x, ord) returns x from the row with the largest ord in each group.
- It aggregates before the shuffle and never sorts, so it beats the window version for "top 1".
- Ties are not deterministic, and nulls in ord are skipped.
- Use a struct to take several columns from the same row.
Check yourself
3 questions1. Two rows in session s9 share the latest ts. What does max_by(page, ts) return for s9?
Show the answer
One of the two pages, and which one is not guaranteed. max_by keeps whichever tied row it meets first, and the order depends on partitioning. Add a tiebreaker to make it deterministic.
2. Why is max_by usually cheaper than row_number() over a window for "latest row per group"?
Show the answer
It aggregates before the shuffle and never sorts. Each partition reduces its rows to one pair per group before the exchange, and no sort is needed. The window shuffles and sorts every row.
3. How do you get page and referrer guaranteed from the same winning row?
Show the answer
max_by(struct(page, referrer), ts). One max_by over a struct picks a single row and returns all its fields together.
Practice it
Interview problems that use max_by / min_by: write the PySpark, run it, and get graded on hidden tests.
Go deeper
Primary sources: functions.max_by · Built-in functions (SQL)