Fundamentals2026-08-28

SQL: Find Rows with the Max Value per Group (3 Clean Solutions)

Get the full row that contains the max value per group in SQL: ROW_NUMBER() window function, NOT EXISTS, and a self-join — with MySQL and PostgreSQL examples.

The Classic Interview Question

"Return the full order row for the most expensive purchase per customer" — this shows up in every SQL interview and in real dashboards daily. The naive answer, GROUP BY customer_id with MAX(amount), gives you the amount but not the rest of the row. Here are the three solutions I actually use, in order of preference.

Solution 1: ROW_NUMBER() Window Function (Best)

SELECT customer_id, amount, order_date, status FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY amount DESC) AS rn FROM orders) t WHERE rn = 1;

Reads cleanly, handles ties deterministically (only one row per group), and the subquery is easy to extend — change ORDER BY to get the latest order instead of the biggest. This is the answer to use in MySQL 8+ and PostgreSQL.

Solution 2: NOT EXISTS (Works Everywhere)

SELECT o.* FROM orders o WHERE NOT EXISTS (SELECT 1 FROM orders o2 WHERE o2.customer_id = o.customer_id AND o2.amount > o.amount);

Semantically "no other row in the same group has a bigger amount". Works on older MySQL 5.x, and it naturally returns all tied rows — if two orders share the same max amount, both come back.

Solution 3: Self-Join + MAX Subquery

SELECT o.* FROM orders o JOIN (SELECT customer_id, MAX(amount) AS max_amount FROM orders GROUP BY customer_id) m ON m.customer_id = o.customer_id AND m.max_amount = o.amount;

This is the pattern people write first because it follows the GROUP BY instinct. It works, but on a big orders table you scan twice. For a few thousand rows nobody notices; past a million, the window function wins.

Which One Should You Use?

My rule of thumb: ROW_NUMBER() unless I specifically need ties (then NOT EXISTS). The self-join is fine as a teaching example but I wouldn't put it in production on a hot table. On PostgreSQL you can also use DISTINCT ON (customer_id) with ORDER BY amount DESC — concise, but it's a Postgres-only extension.

Try It Yourself

Describe your table to our free SQL generator — say "most expensive purchase per customer with the full row" and it'll write the window function version for your dialect.