Skip to content
/ Chapter 10 · Subqueries and EXISTS
Lesson 10.9 · garden_shop
Lesson 10.9

SQL Latest Row per Group

GROUP BY can find each group's latest date but not the row it belongs to. A correlated subquery keeps every row whose date equals its own group's latest.

"Each customer's most recent order" sounds like a GROUP BY question, and max(order_date) does find each customer's latest date. It can't tell you which order that was. Add order_id to the select list and DuckDB stops with column "order_id" must appear in the GROUP BY clause: a group holds several orders, and the query hasn't said which one's id to show.

Why max(order_id) isn't the answer

The tempting fix is to take the highest id as well, on the theory that the newest order has the biggest number:

Highest id against latest date
select customer_id, max(order_id) as highest_id, max(order_date) as latest_date
from orders
where customer_id in (7, 10, 13)
group by customer_id
order by customer_id

For these three customers the highest id is the wrong order. Customer 7's latest order, on April 7, is order 4; order 16 was placed in February. Each max() runs on its own column, so the two maximums can come from different rows. An id records when a row was inserted, not when the order happened; the MAX(id) as latest mistake page shows the same trap from the other side.

Keep the row that matches its group's maximum

Lesson 10.4 compared each product with its own category's average. The same move picks whole rows: compare each order's date with its own customer's latest date, and keep the orders that match.

The latest order per customer
select o.order_id, o.customer_id, o.order_date
from orders as o
where o.order_date = (
  select max(o2.order_date)
  from orders as o2
  where o2.customer_id = o.customer_id
)

For each outer order o, the subquery looks only at that customer's orders, through o2.customer_id = o.customer_id, and returns their latest date. An order survives when it's the one on that date. Because the outer query reads orders directly, with no GROUP BY, every column of the winning row is available: the id, the status, the shipped date. Swap max for min and you get each customer's first order instead.

Ties keep every winner

The comparison is =, so when two rows share a group's maximum, both are kept. No Garden Shop customer placed two orders on one day, but order lines tie often. Here is the largest line, by quantity, in each order:

Largest line per order, with ties
select oi.order_id, p.product_name, oi.quantity
from order_items as oi
join products as p on p.product_id = oi.product_id
where oi.quantity = (
  select max(i2.quantity)
  from order_items as i2
  where i2.order_id = oi.order_id
)
order by oi.order_id, p.product_name

That's 28 rows for 24 orders. In orders 2, 9, 16, and 23 both lines have a quantity of 1, so both are "the largest". Sometimes that's the honest answer. When you need exactly one row per group, you need a tie-breaker, and the correlated form has no clean place for one. That's the job of row_number() with an ORDER BY that names one, filtered with QUALIFY in Lesson 11.6.

DuckDB's shortcut

DuckDB has an aggregate for exactly this question. arg_max(order_id, order_date) returns the order_id from the row with the latest order_date, inside an ordinary GROUP BY:

arg_max in DuckDB
select customer_id, arg_max(order_id, order_date) as latest_order_id
from orders
group by customer_id
order by customer_id

It returns one value per group even when there's a tie, and which tied row it picks isn't defined. DuckDB also spells it max_by, the name Spark, Trino, and Snowflake use. PostgreSQL, MySQL, and SQL Server have no equivalent. The correlated subquery runs on all of them, which is why it's still the form to know. To keep the whole row rather than one value, DuckDB and PostgreSQL also have DISTINCT ON.

Schema · Garden Shop Table · orders 6 columns · 24 rows
Table · orders

One row per order. Unshipped orders have a null shipped_date.

6 columns · 24 rows
order_id int customer_id int order_date date shipped_date date status text coupon_code text
Your task

Return each customer's most recent order: customer_id, order_id, order_date, and status, sorted by customer. The starter finds each customer's latest date but not the order; use a correlated subquery to return the whole row.

·

Run your query to inspect the result.

Expected answer
  • Columns: customer_id, order_id, order_date, status.
  • 16 rows, one per customer who has ordered. Customer 7's latest is order 4, not order 16.
  • No customer has two orders on the same date. If one did, both would appear: = keeps every row that matches.
← Previous · 10.8 IN vs JOIN with duplicates
End of Chapter 10

That's the last lesson in “Subqueries and EXISTS.”

The next chapter builds on what this one covered.

Start Chapter 11: Practical Analytics Patterns → Begins with 11.1 Top-N reports