In a top-3-per-group query, how does RANK() differ from ROW_NUMBER() when the metric ties?
answer
- Only ties make the three functions diverge
- One numbers rows, one numbers places
- Gaps after ties versus no gaps
- rank <= 3 can return more than three rows
- ROW_NUMBER caps the count but picks arbitrarily
basics
~20 sROW_NUMBER() never repeats a number, so rn <= 3 returns at most three rows per group. RANK() gives tied rows the same number, so rank <= 3 can return more than three rows when third place is tied.
solid answer
~50 sThey differ only when the window `ORDER BY` values tie, and the difference decides how many rows survive the outer filter. `ROW_NUMBER()` numbers 1, 2, 3, … with no duplicates, so `WHERE rn <= 3` yields at most three rows per partition — but which of two tied rows gets 2 and which gets 3 is arbitrary unless the window `ORDER BY` has a unique tiebreaker. `RANK()` assigns equal numbers to tied rows and then skips the gap: for salaries 500, 400, 300, 300, 200 the ranks are 1, 2, 3, 3, 5, so `rank <= 3` returns four rows. `DENSE_RANK()` numbers distinct values without gaps (1, 2, 3, 3, 4), so `<= 3` means "the top three salary levels", which can be many rows. Choose by requirement: exactly N rows, N places including ties, or N distinct values.
code
sql · 9 linesSELECT name, salary,
ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn,
RANK() OVER (ORDER BY salary DESC) AS rnk,
DENSE_RANK() OVER (ORDER BY salary DESC) AS drnk
FROM employees;
-- salaries 500, 400, 300, 300, 200 produce:
-- rn = 1, 2, 3, 4, 5 -> rn <= 3 keeps 3 rows
-- rnk = 1, 2, 3, 3, 5 -> rnk <= 3 keeps 4 rows
-- drnk = 1, 2, 3, 3, 4 -> drnk <= 3 keeps 4 rowsgo deeper
Know the three numberings on a tied list by heart: 1,2,3,4 for ROW_NUMBER, 1,2,2,4 for RANK, 1,2,2,3 for DENSE_RANK. Be able to write out the column values for a small example.
Explain how each function changes the row count that survives WHERE rank <= N, and show how a unique column in the window ORDER BY makes ROW_NUMBER deterministic.
Demonstrate that you pick the function from the requirement, not from habit: fixed-size output, tied places included, or top N distinct values — and say what breaks for the caller in each case.
Push the conversation upstream: a tie in the ranking metric is usually an unresolved product decision. Own defining the tiebreaker rule once, in a shared view or model, rather than letting each query invent its own.
## Three functions, one filter Every top-N-per-group query ends in the same predicate — `WHERE rank_column <= N` — but the number of rows that come back depends entirely on which ranking function produced the column. The three candidates differ only in how they treat rows whose window `ORDER BY` values are equal. Take one department with salaries 500, 400, 300, 300 and 200: ```sql SELECT name, salary, ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn, RANK() OVER (ORDER BY salary DESC) AS rnk, DENSE_RANK() OVER (ORDER BY salary DESC) AS drnk FROM employees; -- rn = 1, 2, 3, 4, 5 -- rnk = 1, 2, 3, 3, 5 -- drnk = 1, 2, 3, 3, 4 ``` Filter each column with `<= 3` and you get three different answers: `rn` keeps three rows, `rnk` keeps four (both rows tied at 3), and `drnk` also keeps four here — but with salaries 500, 400, 400, 400, 300 the dense ranks are 1, 2, 2, 2, 3 and `drnk <= 3` keeps all five rows. ## What each one means in business terms **`ROW_NUMBER()`** answers "give me at most N rows per group". It is the right choice when a downstream consumer needs a fixed-size list — a three-slot leaderboard tile, a paging window, a one-row-per-key export. Its cost is arbitrariness: when the metric ties, SQL gives no rule for who wins, so the query can return different rows on different runs. **`RANK()`** answers "give me the top N places, and if the last place is shared, include everyone in it". This is competition ranking: two silver medals and no bronze. It is the honest choice when excluding a tied row would be unfair or wrong, and the price is that the row count is not fixed — the caller must tolerate more than N rows. **`DENSE_RANK()`** answers "give me every row whose metric is among the top N distinct values". Use it for "the three highest salary bands" or "everything from the top three price points". The row count is even less bounded, since a single popular value can carry hundreds of rows. ## Making ROW_NUMBER deterministic If you pick `ROW_NUMBER()` because you need exactly N rows, add a unique column to the window `ORDER BY` so the tie is broken by the query rather than by whatever order rows happen to arrive in: ```sql ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC, employee_id) AS rn ``` Now two employees on the same salary are ordered by `employee_id`, and the query returns the same three rows every time. Without such a tiebreaker the result is unspecified, and "it looked stable in testing" is not a guarantee. ## The diagnostic version of this question Interviewers often approach it from the failure side: "your latest-order-per-customer query returns 8,140 rows but you only have 8,000 customers — why?" The usual cause is `RANK() = 1` (or `DENSE_RANK() = 1`) where `ROW_NUMBER() = 1` was intended: any customer with two orders sharing the top timestamp contributes two rows. Swapping to `ROW_NUMBER()` with a tiebreaker collapses each customer back to a single row. The mirror-image bug is using `ROW_NUMBER()` for a leaderboard and quietly dropping a genuinely tied competitor. ## A caution about the window ORDER BY itself All of this assumes there is a window `ORDER BY`. `ROW_NUMBER() OVER (PARTITION BY department_id)` with no ordering is syntactically legal and numbers the rows in an unspecified order, which turns "top 3" into "any 3" — a bug that will not raise an error and may not show up until the data grows. Similarly, if the metric is nullable, engines differ on whether NULLs sort first or last by default, so a NULL can occupy rank 1 under `DESC`; write `NULLS LAST` explicitly (or exclude NULL metrics in the inner query) when it matters. ## How to answer in an interview State the mechanical difference in one sentence, then immediately tie it to the row count the outer filter produces, and finish by asking which behaviour the requirement wants. That last step is what separates a candidate who memorised three definitions from one who has shipped the query.
- Which function would you use for a leaderboard that must show every competitor tied for third place?`RANK()`. It gives tied rows the same number, so `rank <= 3` returns all competitors sharing third place, then skips the following numbers. `ROW_NUMBER()` would silently drop one of the tied competitors, and `DENSE_RANK()` would additionally pull in the next distinct score because it never leaves gaps.
- A latest-row-per-customer query returns more rows than there are customers. What do you check first?Whether the query filters `RANK() = 1` or `DENSE_RANK() = 1` instead of `ROW_NUMBER() = 1`. Both tie-aware functions return every row sharing the top timestamp, so any customer with two events at the same instant contributes two rows. Switching to `ROW_NUMBER()` with a unique tiebreaker in the window ORDER BY restores one row per customer.
saying these in an interview costs you the question
- Says RANK and ROW_NUMBER are interchangeable
- Claims rank <= 3 always returns exactly three rows
- Confuses DENSE_RANK with RANK on gap behaviour
- Assumes ROW_NUMBER breaks ties consistently across runs
- Thinks ties are impossible because rows are physically distinct