Hook: Interviewers love ROW_NUMBER because it tests two things at once: can you order rows correctly, and do you understand why the result must be deterministic?
Question: What does ROW_NUMBER do in SQL?
Answer: ROW_NUMBER is a window function that assigns a unique number to each row after you tell SQL how to sort the rows. If you use PARTITION BY, the numbering restarts inside each group. It is most useful when you want the first row per group, de-duplication, or clean pagination.
Interview-Ready Answer: I use ROW_NUMBER when I need a unique sequence for rows based on a defined order. It starts at 1 and, if I add PARTITION BY, it restarts for each group. One important detail is that I always add a deterministic tie-breaker in the ORDER BY, because if two rows compare equal, the engine is free to pick either one first.
Detailed Explanation:
ROW_NUMBER() is a window function, meaning it looks at a set of rows related to the current row without collapsing them into one result. A plain aggregate like SUM() gives one value per group; ROW_NUMBER() keeps every row and just adds a sequence number.
FROM, then applies WHERE, GROUP BY, and HAVING.ORDER BY inside OVER.1, the next gets 2, and so on.PARTITION BY, the counter restarts at 1 for each partition.ORDER BY control display order.That last point matters: the numbering order is defined by the window ORDER BY, not the outer query order. If you sort the final output differently, the row numbers do not change.
| Function | Ties | Sequence shape | Best use |
|---|---|---|---|
| ROW_NUMBER | No ties | 1, 2, 3, 4 | Pick exactly one row |
| RANK | Same rank | 1, 1, 3 | Show tied positions |
| DENSE_RANK | Same rank | 1, 1, 2 | Compact ranking |
Memory rule: ROW_NUMBER is the only one that always gives a unique number to every row. If two rows tie, RANK and DENSE_RANK preserve the tie; ROW_NUMBER does not.
The expensive part is usually sorting. The numbering itself is cheap, but sorting a million rows is not. In practice, window queries often behave like an O(n log n) sort plus an O(n) pass to assign numbers. If the data is already supported by an index that matches PARTITION BY and ORDER BY, the optimizer may do less work. If not, large partitions can spill to disk.
The biggest gotcha is non-determinism on ties. If your ORDER BY is not unique, the engine may return a different row as rn = 1 on different runs. Fix that by adding a stable tie-breaker, like a primary key.
Another common trap is trying to filter with WHERE rn = 1 in the same SELECT. Window functions are evaluated after WHERE, so you must wrap the query in a subquery or CTE first.
Real-World Example: Imagine a checkout service that stores every payment attempt. For each customer_id, the team wants the latest successful attempt so support can see the final charge state. They use ROW_NUMBER() partitioned by customer and ordered by event time descending, then keep rn = 1.
What goes wrong when someone forgets the tie-breaker? Two payment rows land in the same second, and the query sometimes picks the older row. Users see the wrong receipt total, support sees mismatched audit trails, and the logs show the same customer returning different row ids after a redeploy. That kind of bug is nasty because the SQL looks correct at first glance, but the sort order was not fully defined.
-- Demonstrates ROW_NUMBER() for "latest row per customer" and the tie-breaker that makes the result deterministic.
-- The sample data includes a deliberate edge case: two rows for customer A share the same timestamp.
-- Without the extra ORDER BY order_id DESC, the winner between those two rows would be undefined.
WITH orders(order_id, customer_id, order_ts, amount) AS (
SELECT 1, 'A', '2024-01-01 10:00:00', 50 UNION ALL
SELECT 2, 'A', '2024-01-01 10:00:00', 60 UNION ALL
SELECT 3, 'A', '2024-01-02 09:00:00', 55 UNION ALL
SELECT 4, 'B', '2024-01-03 08:00:00', 40 UNION ALL
SELECT 5, 'B', '2024-01-01 12:00:00', 45
), numbered AS (
SELECT
order_id,
customer_id,
order_ts,
amount,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY order_ts DESC, order_id DESC
) AS rn
FROM orders
)
SELECT
customer_id,
order_id,
order_ts,
amount,
rn
FROM numbered
ORDER BY customer_id, rn;
-- Correct pattern for filtering to the top row per customer:
-- Window functions cannot be used in WHERE in the same SELECT, so we filter in an outer query.
WITH orders(order_id, customer_id, order_ts, amount) AS (
SELECT 1, 'A', '2024-01-01 10:00:00', 50 UNION ALL
SELECT 2, 'A', '2024-01-01 10:00:00', 60 UNION ALL
SELECT 3, 'A', '2024-01-02 09:00:00', 55 UNION ALL
SELECT 4, 'B', '2024-01-03 08:00:00', 40 UNION ALL
SELECT 5, 'B', '2024-01-01 12:00:00', 45
)
SELECT customer_id, order_id, order_ts, amount
FROM (
SELECT
order_id,
customer_id,
order_ts,
amount,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY order_ts DESC, order_id DESC
) AS rn
FROM orders
) x
WHERE rn = 1
ORDER BY customer_id;Follow-up & Tricky Questions:
Follow-up questions
ROW_NUMBER different from RANK? ROW_NUMBER never ties: every row gets a unique number. RANK gives equal rows the same value and leaves gaps after ties.ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ... DESC) and keep rn = 1. Add a primary key as the last sort column so the result is stable.rn directly in WHERE? Because window functions are computed after WHERE. Put the window query in a subquery or CTE, then filter outside it.PARTITION BY? The numbering runs across the entire result set as one big group, starting at 1.ORDER BY change the row numbers? No. It only changes the final display order; the window ORDER BY controls the numbering itself.Tricky / gotcha questions
ORDER BY, is the result stable? No. SQL does not guarantee which tied row gets the lower number unless you add another sort column that makes the order unique.ROW_NUMBER be used for pagination safely? Yes, but only with a deterministic sort and ideally a stable snapshot; otherwise rows can move between pages as data changes.ROW_NUMBER the same as numbering after GROUP BY? No. GROUP BY changes the row set; ROW_NUMBER numbers the existing rows after the grouping step is finished.Common Mistakes:
ORDER BY.WHERE rn = 1 in the same SELECT. Fix: compute rn in a CTE or subquery, then filter outside.ROW_NUMBER with RANK. Fix: remember that ROW_NUMBER never repeats numbers, while RANK can.ORDER BY inside OVER decides the row numbers.Memory Hook: Think of ROW_NUMBER as a deli ticket machine: you choose the line order first, then each row gets the next ticket, and the ticket count resets when a new line starts.
Cheat Sheet:
PARTITION BY = restart numbering per group.ORDER BY inside OVER = numbering rule.WHERE.Practice Tasks:
rn = 1.