Step 1 Understand it
Why you need it. "Top 3 per department", "the second-highest salary", "each customer's first order" — ranking questions are the most common -function questions in interviews. The only hard part is ties, and the three functions differ exactly there.
New words in this topic (tap one to see what it means)
Watch it happen
Read the animation as text
- Sorted by salary, highest first. Ben and Chen tie on 70000 — the three ranking functions differ only in how they treat a tie.
- ROW_NUMBER: 1, 2, 3, 4… always different. Ben and Chen get 3 and 4; which way round is arbitrary unless you add a tiebreaker (here, name).
- RANK: ties share a number, then it skips — like sports. Ben and Chen are both 3, so nobody is 4: Dara is 5.
- DENSE_RANK: ties share a number, with no gap. Dara is 4. "The 2nd highest salary" is simply DENSE_RANK = 2.
Picture it
flowchart LR S["salaries, high to low<br/>95000 · 80000 · 70000 · 70000 · 60000"] --> R1["ROW_NUMBER<br/>1 · 2 · 3 · 4 · 5"] S --> R2["RANK<br/>1 · 2 · 3 · 3 · 5"] S --> R3["DENSE_RANK<br/>1 · 2 · 3 · 3 · 4"]
The idea
All three need inside , which decides who comes first. always gives different numbers, so if two rows tie, which one gets 3 and which gets 4 is arbitrary unless you add a . is like a sports ranking: two people sharing 3rd place means nobody is 4th. doesn't leave gaps, so "the 2nd highest salary" is simply DENSE_RANK = 2. Add to restart the numbering in each group.
The shape of it
ROW_NUMBER() OVER (PARTITION BY g ORDER BY x DESC, tiebreaker)
RANK() OVER (PARTITION BY g ORDER BY x DESC)
DENSE_RANK() OVER (PARTITION BY g ORDER BY x DESC)Words in lower case are the parts you fill in.
How it works, step by step
- Sort by salary, highest first: Asha 95000, Hana 80000, then Ben and Chen on 70000, then Dara 60000.
- ROW_NUMBER: 1, 2, 3, 4, 5 — Ben and Chen get different numbers; the tiebreaker (name) puts Ben first.
- RANK: 1, 2, 3, 3, 5 — they share 3, and 4 is skipped.
- DENSE_RANK: 1, 2, 3, 3, 4 — they share 3, and Dara is 4.
- To keep only the top rows, compute the rank in a and filter with outside it.
Analogy
A race with two runners crossing the line together in third. ROW_NUMBER is the finish-line camera that must put one of them first. RANK is the medal : both get third, and the next runner is fifth. DENSE_RANK is "positions": both third, the next runner fourth.
Where the analogy breaks
A photo finish has a real answer. ROW_NUMBER's choice between tied rows doesn't — it can differ between runs — so always add a tiebreaker (like name or id) when you use ROW_NUMBER.
Worked examples
The three side by side
SELECT name, salary,
ROW_NUMBER() OVER (ORDER BY salary DESC, name) AS row_num,
RANK() OVER (ORDER BY salary DESC) AS rank_num,
DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_num
FROM employees
ORDER BY salary DESC, name;8 rows
| name | salary | row_num | rank_num | dense_num |
|---|---|---|---|---|
| Asha | 95000 | 1 | 1 | 1 |
| Hana | 80000 | 2 | 2 | 2 |
| Ben | 70000 | 3 | 3 | 3 |
| Chen | 70000 | 4 | 3 | 3 |
| Dara | 60000 | 5 | 5 | 4 |
| Fatima | 52000 | 6 | 6 | 5 |
| Eli | 45000 | 7 | 7 | 6 |
| Gus | 38000 | 8 | 8 | 7 |
Look at Ben and Chen, then at Dara: 5 by RANK, 4 by DENSE_RANK.
Ranking within each department
SELECT dept_id, name, salary,
DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rank_in_dept
FROM employees
WHERE dept_id IS NOT NULL
ORDER BY dept_id, rank_in_dept, name;7 rows
| dept_id | name | salary | rank_in_dept |
|---|---|---|---|
| 1 | Asha | 95000 | 1 |
| 1 | Hana | 80000 | 2 |
| 1 | Ben | 70000 | 3 |
| 1 | Chen | 70000 | 3 |
| 2 | Dara | 60000 | 1 |
| 2 | Eli | 45000 | 2 |
| 3 | Fatima | 52000 | 1 |
PARTITION BY restarts the count at 1 for every department.
Filter on a rank: each customer's second order
WITH numbered AS (
SELECT customer_id, order_id, order_date,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date) AS nth_order
FROM orders
)
SELECT customer_id, order_id, order_date
FROM numbered
WHERE nth_order = 2
ORDER BY customer_id;3 rows
| customer_id | order_id | order_date |
|---|---|---|
| 1 | 102 | 2024-01-07 |
| 2 | 108 | 2024-03-01 |
| 3 | 107 | 2024-02-20 |
The number is made in the CTE, then filtered outside. Zoe has only one order, so she has no second.
Must-know points
- ROW_NUMBER: unique 1, 2, 3, 4. Add a tiebreaker so it is the same every run.
- RANK: ties share a number, then a gap (1, 2, 2, 4).
- DENSE_RANK: ties share a number, no gap (1, 2, 2, 3). Use it for "Nth highest value".
- PARTITION BY restarts the numbering per group.
- Pattern for : rank in a CTE, then
WHERE rank <= Noutside.
Common mistakes
- Using ROW_NUMBER for "top 3 salaries" when ties should all be included — use RANK or DENSE_RANK.
- Using RANK for "the 2nd highest salary" — after a tie at 1st, there is no rank 2.
- Forgetting the tiebreaker in ROW_NUMBER, so results change from run to run.
In other databases
- Naming a column
rankfails in 8, where RANK is a reserved word. Use rank_num or rnk.
Step 2 Go further (free, optional)
Optional. The notes above are written to be enough on their own. These free resources give a second explanation and more practice.
-
Read
PostgreSQL docs — Window Functions (reference)
official documentation, free
- Read
-
Practice
DataLemur — ranking window function questions
real interview questions, many free
Step 3 Practise
Try each one yourself first: out loud, or written and run in the practice box below, which saves every query you run. Then open the answer and compare.
-
ExplainSay what it does and when to use it, in plain words, with no notes.
Explain the difference between RANK and DENSE_RANK using Ben, Chen and Dara.
Show answer
Sorted by salary, highest first: Asha 95,000 (1st), Hana 80,000 (2nd), then Ben and Chen tie on 70,000, then Dara on 60,000.
Both functions give Ben and Chen the same number, 3. The difference is what comes next:
• RANK skips: two people took places 3 and 4, so Dara is 5. Like a race: two runners share third, and the next one finishes fifth.
• DENSE_RANK doesn't skip: Dara is 4, the next different salary.So "the third-highest salary" is DENSE_RANK = 3, while "the person in fourth place" may not exist with RANK.
-
Writefrom a blank page, then run it
Write a for the most expensive product in each category, using ROW_NUMBER.
Show answer
Number the products inside each category from most to least expensive, then keep number 1. The CTE is needed because WHERE runs before window functions, so you can't filter on rn in the same query.
ROW_NUMBER always picks exactly one product, even if two tie on price. product_name is added to the ORDER BY so that pick is fixed, not random. To keep every tied product, use RANK instead.
sqlWITH ranked AS ( SELECT category, product_name, price, ROW_NUMBER() OVER (PARTITION BY category ORDER BY price DESC, product_name) AS rn FROM products ) SELECT category, product_name, price FROM ranked WHERE rn = 1 ORDER BY category;5 rows
category product_name price Bags Backpack 30 Electronics Headphones 40 Home Desk lamp 25 Kitchen Mug 8 Stationery Notebook 5 -
Predictsay it before you run it
With
RANK() OVER (ORDER BY salary DESC), which employees have rank 3? Who has rank 4?Show answer
Rank 3: Ben and Chen, who tie on 70,000.
Rank 4: nobody. The tie used up places 3 and 4, so RANK jumps straight to 5 for Dara.
sqlSELECT name, salary, RANK() OVER (ORDER BY salary DESC) AS rank_num FROM employees ORDER BY salary DESC, name;8 rows
name salary rank_num Asha 95000 1 Hana 80000 2 Ben 70000 3 Chen 70000 3 Dara 60000 5 Fatima 52000 6 Eli 45000 7 Gus 38000 8 -
Edge casesSay what happens with NULLs, ties, duplicates or no rows at all, and fix it.
Rewrite "each customer's latest order" so that, if two orders shared the latest date, only one comes back — and say which one.
Show answer
Use ROW_NUMBER, which never gives two rows the same number, and add a tie-breaker to its ORDER BY so you decide which one wins:
ORDER BY order_date DESC, order_id DESC. Here, if two orders shared the latest date, the one with the higher order_id (the one created later) comes back.Without the tie-breaker, ROW_NUMBER still returns one row, but which one could change from run to run. RANK or DENSE_RANK would return both.
(No customer here has two orders on the same day, so the tie-breaker changes nothing yet; it is there for when one does.)
sqlWITH numbered AS ( SELECT customer_id, order_id, order_date, ROW_NUMBER() OVER ( PARTITION BY customer_id ORDER BY order_date DESC, order_id DESC ) AS rn FROM orders ) SELECT customer_id, order_id, order_date FROM numbered WHERE rn = 1 ORDER BY customer_id;4 rows
customer_id order_id order_date 1 109 2024-03-10 2 108 2024-03-01 3 107 2024-02-20 5 110 2024-03-16
Practice box: write and run your answers here
Your queries in this box
Step 4 Interview-ready, then prove it
Your interview answer
About 20 to 40 seconds, said out loud.
All three number rows in the order given by the OVER clause's ORDER BY, optionally restarting per PARTITION BY. ROW_NUMBER gives unique sequential numbers, so I add a deterministic tiebreaker; RANK gives tied rows the same rank and leaves gaps; DENSE_RANK gives the same rank without gaps. For "Nth highest distinct value" I use DENSE_RANK, and for "exactly one row per group" I use ROW_NUMBER — always filtering in an outer query.
Follow-up questions
Which would you use for "the second highest salary"?
DENSE_RANK = 2, so a tie for first doesn't leave you with no second.
How do you keep exactly one row per customer — their latest order?
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date DESC, order_id DESC), then keep row number 1.
What happens to ROW_NUMBER with ties if there is no tiebreaker?
Tied rows get different numbers in an arbitrary order, which can change between runs.
Tick each check when you can do it without notes
A topic is finished when you pass all four checks, not when you have read it. Be strict: if you had to look at the notes, it is not ticked yet.
- Explain Say what it does and when to use it, in plain words, with no notes.
- Write Write the query from a blank page, then run it to check.
- Predict Read a query and say which rows it returns before you run it.
- Edge cases Say what happens with NULLs, ties, duplicates or no rows at all, and fix it.
0 of 4 checks ticked.
Saved in this browser. Back it up or move it to another device.