Module 5 · Advanced SQL
Top N per group and deduplication
Return the top rows within every group, the latest record per entity, and one clean row out of several, choosing deliberately how ties are handled.
About 20 minutes
The problem
Three requests that look different but are the same problem:
- Sales: "Our top three customers for each transport mode, by revenue."
- Finance: "For each shipment, the most recent payment."
- Data team: "The CRM export has some customers in it twice. Keep one row per customer: the newest."
ORDER BY … LIMIT 3 gives the top three overall, not per mode. GROUP BY with MAX(payment_date) finds the latest date but loses the rest of the row (the amount, the method). Each request needs "the top N rows within each group, with the whole row".
The concept
The pattern has three steps:
- Number the rows within each group with a window function, in the order that defines "top".
- Do it in a CTE or subquery, because
WHEREcan't see window functions in the same query. - Filter on the number outside.
SQL
WITH ranked AS (
SELECT …, ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY sort_col DESC, id) AS rn
FROM …
)
SELECT … FROM ranked WHERE rn <= 3;Choosing the numbering function decides what happens to ties:
| Function | Ties | Use when |
|---|---|---|
ROW_NUMBER() | broken arbitrarily, so add a tie-breaker | you need exactly N rows, such as deduplication |
RANK() | tied rows share a rank, and the next one skips | "everyone in the top 3, including ties" |
DENSE_RANK() | tied rows share a rank, no gaps | "the top 3 values", such as the three highest prices |
Always add a tie-breaker to ROW_NUMBER (usually the ID). Without one the database may pick a different row each time you run the query, and two people running the same report get different answers.
Example
The top three customers by delivered freight charges, within each mode:
WITH customer_mode AS (
SELECT r.mode, s.customer_id, SUM(s.freight_charge) AS charges
FROM shipments AS s
JOIN routes AS r ON r.route_id = s.route_id
WHERE s.status = 'Delivered'
GROUP BY r.mode, s.customer_id
),
ranked AS (
SELECT
mode,
customer_id,
charges,
ROW_NUMBER() OVER (PARTITION BY mode ORDER BY charges DESC, customer_id) AS rn
FROM customer_mode
)
SELECT ranked.mode, ranked.rn, c.company_name, ranked.charges
FROM ranked
JOIN customers AS c ON c.customer_id = ranked.customer_id
WHERE ranked.rn <= 3
ORDER BY ranked.mode, ranked.rn;Nine rows: three per mode. Note the order of the work. First total per customer and mode, then number within each mode, then filter, and only then join the names in. Joining names at the end means the ranking runs on fewer, narrower rows.
Walkthrough
- Run the example. Then change
rn <= 3torn = 1to get each mode's single biggest customer. - Replace
ROW_NUMBER()withRANK(). Nothing changes here because no two customers have identical charges, but on a column with ties, such ascontainers,RANKcould return more than three rows per group. - Now the "latest record" version. For shipments paid in instalments, keep only the most recent payment, with the full row:
WITH numbered AS (
SELECT
p.*,
ROW_NUMBER() OVER (PARTITION BY shipment_id ORDER BY payment_date DESC, payment_id DESC) AS rn,
COUNT(*) OVER (PARTITION BY shipment_id) AS payments
FROM payments AS p
)
SELECT shipment_id, payment_id, payment_date, amount, method, payments
FROM numbered
WHERE rn = 1 AND payments > 1
ORDER BY shipment_id
LIMIT 20;- Check the count without
LIMIT: 159 shipments have more than one payment. Deduplication works the same way: number the copies newest first and keeprn = 1.
Practice
Practice
For each route, show its most recent booking. Show route_id, shipment_id and booking_date (break ties by the higher shipment_id), ordered by route_id.
Ctrl + Enter to run. Tab indents; press Esc then Tab to leave the editor.
Practice
For each customer, find their busiest route by number of shipments. If two routes tie, show both. Show customer_id, route_id and shipments, ordered by customer_id then route_id.
Ctrl + Enter to run. Tab indents; press Esc then Tab to leave the editor.
Challenge
Challenge · optional
For each mode, show the shipments with the second-highest freight_charge value (if several share that value, show them all). Show mode, shipment_id and freight_charge, ordered by mode then shipment_id.
Ctrl + Enter to run. Tab indents; press Esc then Tab to leave the editor.
More practice
Drill · optional
For each year (from booking_date), show its two busiest months. Show year, month ('YYYY-MM') and shipments, ordered by year then shipments descending.
Ctrl + Enter to run. Tab indents; press Esc then Tab to leave the editor.
Drill · optional
For each account manager, show their single largest customer by number of shipments. Show account_manager_id, customer_id and shipments (break ties by the lower customer_id), ordered by account_manager_id. Leave out customers with no account manager.
Ctrl + Enter to run. Tab indents; press Esc then Tab to leave the editor.
Check your understanding
Answer every question to check.