How do you use LATERAL to return each customer's three most recent orders?
answer
- one row per group on the left
- the row limit lives inside the parentheses
- sorting is scoped to the current left row
- equal timestamps make the choice arbitrary
- empty groups vanish under the inner form
basics
~20 sDrive the query from one row per group and join a LATERAL subquery that selects from the detail table, correlates on the group key, and carries its own ORDER BY with FETCH FIRST 3 ROWS ONLY. The limit then applies per left row, not to the whole result.
solid answer
~50 sPut the groups on the left and the per-group top-N inside the LATERAL: ```sql SELECT c.name, o.id, o.placed_at FROM customers c CROSS JOIN LATERAL (SELECT o.id, o.placed_at FROM orders o WHERE o.customer_id = c.id ORDER BY o.placed_at DESC, o.id DESC FETCH FIRST 3 ROWS ONLY) AS o; ``` Because the subquery is evaluated per left row, its `ORDER BY` and row limit are scoped to that customer's orders — this is the one place in SQL where a row limit is naturally per group. Three details matter: the left side must be a list of *distinct* groups (use `SELECT DISTINCT customer_id FROM orders` if there is no customers table); the `ORDER BY` needs a unique tiebreaker or the choice among equal timestamps is arbitrary; and `CROSS JOIN LATERAL` drops customers with no orders, so switch to `LEFT JOIN LATERAL ... ON TRUE` when the report must list everyone.
code
sql · 8 lines-- Three most recent orders per customer
SELECT c.id, c.name, o.order_id, o.placed_at
FROM customers c
CROSS JOIN LATERAL (SELECT o.id AS order_id, o.placed_at
FROM orders o
WHERE o.customer_id = c.id
ORDER BY o.placed_at DESC, o.id DESC -- unique tiebreaker
FETCH FIRST 3 ROWS ONLY) AS o;go deeper
Recall that a row limit normally applies to the whole result, and that placing it inside a per-row subquery is what makes it apply per group.
Write the query from memory: distinct groups on the left, correlation plus ORDER BY plus row limit inside the LATERAL, and know that the inner form drops empty groups.
Demonstrate the production details — a unique tiebreaker for determinism, WITH TIES versus a hard cut, a de-duplicated left side, and a conscious choice between the inner and outer form.
Own how the team expresses top-N per group consistently across the query library, including which formulation survives on every engine you deploy to and how reviewers spot the duplicated-left-side bug.
## The shape of the pattern "Top N rows per group" has a natural LATERAL formulation: one row per group on the left, a per-row query on the right. ```sql SELECT c.id, c.name, o.id AS order_id, o.placed_at, o.total FROM customers c CROSS JOIN LATERAL (SELECT o.id, o.placed_at, o.total FROM orders o WHERE o.customer_id = c.id ORDER BY o.placed_at DESC, o.id DESC FETCH FIRST 3 ROWS ONLY) AS o; ``` Read it as a loop over the left input: for each customer, run the inner query against that customer's orders, sort them newest first, keep three, and emit those three rows paired with the customer's columns. Ten customers yield at most thirty rows. ## Why the limit is per group This is the crux of the question. A row limit written in an ordinary query applies to the query's whole result — `SELECT ... FROM orders ORDER BY placed_at DESC FETCH FIRST 3 ROWS ONLY` gives you three orders in total, not three per customer. Inside a LATERAL derived table the limit belongs to a subquery that is evaluated once per left row, so its scope is that row's matching detail rows. The nesting is what makes the limit per-group; nothing about `FETCH FIRST` itself changed. `FETCH FIRST n ROWS ONLY` is the standard spelling; `LIMIT n` is the common alternative on several engines. Both go inside the parentheses, after the subquery's own `ORDER BY`. ## Where the group list comes from The left side must contain each group exactly once. A `customers` table is the obvious source. If you only have the detail table, generate the groups explicitly: ```sql SELECT g.customer_id, o.id, o.placed_at FROM (SELECT DISTINCT customer_id FROM orders) AS g CROSS JOIN LATERAL (SELECT o.id, o.placed_at FROM orders o WHERE o.customer_id = g.customer_id ORDER BY o.placed_at DESC, o.id DESC FETCH FIRST 3 ROWS ONLY) AS o; ``` If the left side accidentally contains a group twice — a join that fanned out, or a forgotten `DISTINCT` — the LATERAL runs twice for it and you get six rows instead of three. When results look duplicated, count the left side first. ## Ties and determinism `ORDER BY o.placed_at DESC FETCH FIRST 3 ROWS ONLY` is non-deterministic whenever the fourth-newest order shares a timestamp with the third: the engine may return either, and it may return a different one on the next run. Two honest answers exist. Add a unique tiebreaker (`, o.id DESC`) so the ordering is total and exactly three rows come back; or, where the engine supports it, use `FETCH FIRST 3 ROWS WITH TIES`, which keeps every row tied with the last one and can therefore return more than three. Say which behaviour the report needs — an interviewer is listening for the awareness, not for one particular choice. ## Keeping groups with no detail rows As written, a customer with zero orders contributes nothing, because `CROSS JOIN LATERAL` is inner. For a dashboard that must list every customer, use the outer form: ```sql FROM customers c LEFT JOIN LATERAL (...) AS o ON TRUE ``` That emits one NULL-extended row for such a customer. Decide deliberately which behaviour you want; the difference is invisible until someone notices a missing name. ## Variations N = 1 gives "the latest row per group", the most common form of all, and LATERAL handles it without any special casing. The inner query can also compute rather than fetch — an aggregate over the group, or several derived values — since it is an ordinary subquery. And the pattern composes: the result of the LATERAL join can be aggregated, filtered or joined onward like any table. ## A note on alternatives A ranking window function filtered in an enclosing query expresses the same requirement, and correlated subqueries can do it too; those formulations have their own trade-offs and their own place. What is specific to LATERAL is that the per-group limit is written exactly where you would say it in English — sort *this customer's* orders, take three — and that the subquery may return several columns from the chosen rows at once.
- What happens to a customer who has never ordered?Under CROSS JOIN LATERAL the customer produces no output row at all, because inner-join semantics drop a left row whose subquery returned nothing. Use LEFT JOIN LATERAL (...) ON TRUE to keep the customer with NULLs in the order columns.
- Why add o.id to the LATERAL's ORDER BY when you are sorting by placed_at?To make the ordering total. If two orders share the newest placed_at, FETCH FIRST 3 ROWS ONLY picks among them arbitrarily and the result can differ between runs. A unique tiebreaker makes it deterministic; FETCH FIRST 3 ROWS WITH TIES, where supported, instead returns every tied row and can exceed three.
- You get six rows for one customer instead of three. What do you check first?The left-hand side. The LATERAL runs once per left row, so a duplicated group — a customers list already fanned out by an earlier join, or a missing DISTINCT — evaluates the subquery twice and doubles the output. Count the rows of the left input in isolation.
saying these in an interview costs you the question
- Putting the row limit in the outer query instead of the subquery
- Expecting per-group results from a single global ORDER BY and LIMIT
- Ignoring ties and calling the result deterministic
- Using a detail table with duplicate group keys as the left side
- Assuming groups with no detail rows still appear