Number the rows inside each group with ROW_NUMBER(), then keep number 1: WITH ranked AS (SELECT *, ROW_NUMBER() OVER (PARTITION BY customer ORDER BY placed DESC) AS rn FROM orders) SELECT * FROM ranked WHERE rn = 1. PARTITION BY names the group, ORDER BY decides which row counts as first, and rn <= 3 gives the top three per group instead.
GROUP BY collapses each group into one summary row, so it can tell you a customer's latest order date but not the rest of that order. To get the whole row (the latest order per customer, the best lap per driver) you rank the rows within each group and filter on the rank. The examples run on SQLite, which has supported window functions since 3.25; the table in section 7 gives the PostgreSQL, MySQL and SQL Server forms. Sections 3 and 4 show the older subquery answers and the duplicate rows they return when two rows tie, and section 6 covers a SQLite shortcut that other databases reject. To glue a group's values into one string instead, see combine rows into one string. Each example runs on this page: hit Run, then edit the query and run it again.
1ROW_NUMBER() in a CTE, then WHERE rn = 1Recommended
ROW_NUMBER() OVER (PARTITION BY customer ORDER BY placed DESC) restarts the count at 1 for every customer and hands out 1 to the newest order. You can't filter on a window function in the same query's WHERE (it is computed after WHERE runs), so put the numbering in a CTE or a derived table and filter outside it. Flip the sort to ASC for the first order instead of the last.
Output
One row per customer, each the full order: ana's order 6 from 2026-03-14, ben's order 5 and cy's only order, 4. The id DESC in the window's ORDER BY is a tie-breaker. ROW_NUMBER() always returns exactly one row numbered 1 per group, but if two orders share a date, which one gets the 1 is up to the database unless the sort settles it. With a unique column last in the ORDER BY, the answer is the same on every run. Add more columns to PARTITION BY to group by several columns at once.
2Top N per group: rn <= N
The same numbering answers "the two most recent orders per customer": filter on rn <= 2 instead of rn = 1. Selecting rn as well shows how the count restarts for each customer.
Output
ana has three orders and returns her two newest, numbered 1 and 2; her order from 2026-03-02 is rank 3 and drops out. cy has only one order, so cy returns one row: a group smaller than N simply returns all of its rows. A plain LIMIT 2 on the whole query couldn't do this, because it counts rows across every customer, not within each one.
3Join to a MAX() subquery, and the tie problem
Before window functions, the usual answer was two steps: a GROUP BY subquery finds each customer's latest date, and a join back to the table fetches the orders on that date. It runs anywhere, but it matches on a value, not on a row, so every order that shares the latest date comes back. This example adds a second order for ben on his last day.
Output
Four rows for three customers: ben's orders 5 and 7 both match his latest date. Sometimes that is what you want, but code that expects one row per customer breaks quietly. With the same seven rows, the ROW_NUMBER() query from section 1 returns a single row for ben, order 7, because its id DESC tie-breaker ranks it first. If you keep the join, make the latest value unique (a full timestamp, or a second MAX(id) step), or switch to section 5's RANK() so the ties are a deliberate choice.
4A correlated subquery, or NOT EXISTS
A correlated subquery asks the same question row by row: keep this order if its date equals the latest date for its own customer. The inner query refers to the outer row through the alias o. The NOT EXISTS form turns it around and keeps an order when no later order exists for the same customer.
Output
Both queries return orders 6, 5 and 4, the same rows as section 1. Both also share the join's tie problem: add section 3's order 7 and each one returns ben twice, orders 5 and 7. They are short to write and work in every database, and with an index on (customer, placed) the inner lookup is cheap. Without that index the database may rerun the subquery for every row, so on a large table check the plan or prefer ROW_NUMBER().
5RANK() to keep ties on purpose
When a tie should return every tied row, say both laps if a driver set the same best time twice, use RANK() in place of ROW_NUMBER(). Rows with equal sort values get the same rank, so WHERE rnk = 1 keeps all of them. This example selects both numbers side by side.
Output
kai's laps 2 and 3 both ran 81.4, so both have rnk 1 and both come back, while their rn values are 1 and 2: filtering on rn = 1 would have kept only lap 2. mo and zoe have a single fastest lap each. RANK() skips numbers after a tie (the next kai lap would be rank 3); DENSE_RANK() doesn't, which matters when you filter on rnk <= N and want N distinct times rather than N positions.
6SQLite only: bare columns next to MAX()
SQLite lets you select columns that are neither grouped nor aggregated. When the query has a single MAX() or MIN(), SQLite fills those bare columns from the row that holds the maximum or minimum, so GROUP BY customer with MAX(placed) returns each customer's latest order in one short query. It is documented SQLite behaviour, and it doesn't carry over to other databases.
Output
SQLite returns orders 6, 5 and 4 with their own totals, the same rows as section 1. PostgreSQL refuses the query with ERROR: column "orders.id" must appear in the GROUP BY clause or be used in an aggregate function, and SQL Server rejects it too. MySQL 8.4 refuses it under its default ONLY_FULL_GROUP_BY mode with ERROR 1055 (42000); with that mode switched off it runs, but it doesn't look for the max row: on the same data it returned ana with id 1 and total 40, values from her first order beside her latest date. Use this shortcut only in SQLite-only code, and with one MAX() or MIN() in the query.
7Other databases: PostgreSQL, MySQL and SQL Server
The ROW_NUMBER() query from section 1 runs unchanged on PostgreSQL, MySQL 8 and SQL Server (Oracle has ROW_NUMBER() as well). Each database also has its own shortcut, and the per-group subquery forms (LATERAL, CROSS APPLY) are handy for top N per group when an index on (customer, placed) exists.
| Syntax | Database | Notes |
|---|---|---|
| ROW_NUMBER() OVER (PARTITION BY … ORDER BY …) | PostgreSQL, MySQL 8+, SQL Server | The portable answer: wrap it in a CTE and filter rn = 1. MySQL 5.7 has no window functions. |
| SELECT DISTINCT ON (customer) … ORDER BY customer, placed DESC | PostgreSQL | Keeps the first row of each customer by the ORDER BY, which must start with the DISTINCT ON columns. |
| CROSS JOIN LATERAL (SELECT … ORDER BY placed DESC LIMIT 2) | PostgreSQL, MySQL 8.0.14+ | Runs the subquery once per customer; LIMIT sets N. |
| CROSS APPLY (SELECT TOP 2 … ORDER BY placed DESC) | SQL Server | The T-SQL form of LATERAL; TOP sets N. |
| SELECT TOP 1 WITH TIES … ORDER BY ROW_NUMBER() OVER (…) | SQL Server | One row per group with no CTE. Not run here: Babelfish doesn’t support WITH TIES. |
Frequently asked questions
How do I get the last record in each group in SQL?
Sort the window descending: ROW_NUMBER() OVER (PARTITION BY customer ORDER BY placed DESC) AS rn in a CTE, then WHERE rn = 1. "Last" needs a column that defines the order, such as a date or an increasing id; a table has no built-in row order, so without an ORDER BY there is no last row.
How do I select the rows with the max value in each group?
Rank by that column descending and keep rank 1. Use ROW_NUMBER() for exactly one row per group, or RANK() to return every row that shares the maximum. Joining the table to SELECT customer, MAX(placed) FROM orders GROUP BY customer also works, but it returns every tied row whether you want them or not.
How do I get the top N rows per group?
Filter the numbered rows on rn <= N instead of rn = 1. With rn <= 2, a customer with three orders returns two rows and a customer with one order returns one. In PostgreSQL and MySQL a CROSS JOIN LATERAL (... LIMIT N) subquery does the same, and in SQL Server CROSS APPLY (SELECT TOP N ...).
Why do I get "must appear in the GROUP BY clause" when I select other columns?
Every selected column must be grouped or aggregated, because a group can hold many different values for it. PostgreSQL reports column "orders.id" must appear in the GROUP BY clause or be used in an aggregate function, and MySQL reports error 1055 under ONLY_FULL_GROUP_BY. Adding the column to GROUP BY changes the groups, so to get the other columns of the max row, use ROW_NUMBER() instead.
What is the difference between ROW_NUMBER, RANK and DENSE_RANK here?
ROW_NUMBER() numbers 1, 2, 3 even when values tie, so rn = 1 is always one row. RANK() gives tied rows the same number and then skips (1, 1, 3), so rnk = 1 returns every tied first row. DENSE_RANK() gives ties the same number without gaps (1, 1, 2).
Does SQLite or MySQL support ROW_NUMBER()?
SQLite has window functions, including ROW_NUMBER(), RANK() and DENSE_RANK(), since 3.25 (2018); the examples here run on SQLite 3.53. MySQL added them in 8.0. On MySQL 5.7 use the join-to-MAX or correlated subquery forms.
Run it yourself
Open any of these queries in the full SQL editor to change the data, run and share.
SQLite playground