What's the difference between ROW_NUMBER, RANK and DENSE_RANK?
Watch the three ranking functions handle the same tie, and pick the one your question needs.
Free course. Your Maestro, an AI, .
At the Norte branch, March and July 2025 sold exactly the same amount: 132,700 pesos. If you sort the months from highest to lowest sales, what place does each one get? It depends on the function:
ROW_NUMBER()counts 1, 2, 3... and never repeats. Two tied rows get different numbers.RANK()gives tied rows the same number and then skips: if two tie at 9, the next one is 11.DENSE_RANK()gives tied rows the same number and doesn't skip: if two tie at 9, the next one is 10.
All three are written the same way, with OVER (ORDER BY ventas DESC). None takes arguments: what changes is how they count when two rows have the same value in that ORDER BY. DuckDB's documentation calls those tied rows peers.
Try it
Norte's January sold 128,900 pesos and comes right after the tie. Before you run anything, commit to the three numbers you think it gets. Then write the query with all three functions at once.
Check yourself: Before you run it: Norte's March and July tie for 9th place. January comes right after. What do ROW_NUMBER, RANK and DENSE_RANK give January, in that order?
For the Norte branch, return mes, ventas and three columns, all from highest to lowest sales: fila with ROWNUMBER() (break ties with mes), rango with RANK() and rangodenso with DENSERANK(). Sort the result by fila. You're done when you see 12 rows and March and July have the same
Your turn
For the Norte branch, return mes, ventas and three columns, all from highest to lowest sales: fila with ROW_NUMBER() (break ties with mes), rango with RANK() and rango_denso with DENSE_RANK(). Sort the result by fila. You're done when you see 12 rows and March and July have the same rango.
Tables: ventas_mensuales (sucursal, mes, ventas)
ventas_mensuales · 35 rows
| Column | Type |
|---|---|
| sucursal Nombre de la sucursal: Centro, Norte o Sur. | VARCHAR |
| mes Primer día del mes que resume la fila. | DATE |
| ventas Ventas del mes, en pesos. | BIGINT |
First rows
| sucursal | mes | ventas |
|---|---|---|
| Centro | 2025-01-01 | 165100 |
| Centro | 2025-02-01 | 155700 |
| Centro | 2025-03-01 | 178700 |
Every run starts from fresh copies of the course's tables, so try anything. Tab indents; to leave the editor, press Escape, then Tab.
Ready to run your query.
These are the rows around the tie, exactly as the solution returned them in Maestro's sandbox:
| mes | ventas | fila | rango | rango_denso |
|---|---|---|---|---|
| 2025-04-01 | 137900 | 7 | 7 | 7 |
| 2025-10-01 | 137200 | 8 | 8 | 8 |
| 2025-03-01 | 132700 | 9 | 9 | 9 |
| 2025-07-01 | 132700 | 10 | 9 | 9 |
| 2025-01-01 | 128900 | 11 | 11 | 10 |
| 2025-02-01 | 127300 | 12 | 12 | 11 |
Above the tie, all three columns agree. That's why it's so easy to believe they're interchangeable: most examples have no ties. The difference shows up right at the tie and after it.
Which one should you use? Ask yourself what you want to happen with a tie:
- A unique number per row (to keep exactly one):
ROW_NUMBER(), with a tiebreaker in theORDER BY. - "Everyone in the top 3 places," ties included:
RANK(), which counts how many finished above. - "The 3 highest values," even if several rows share them:
DENSE_RANK().
Here are Norte's twelve months sorted from highest to lowest sales. The two highlighted bars are the tie: same height, same value. For RANK() and DENSE_RANK() they're the same place; for ROW_NUMBER() one has to go before the other.
The ties you'll run into
A tie at the top. If two months tie for first, RANK() gives 1, 1 and then 3: there's no second place. DENSE_RANK() gives 1, 1 and 2. If your report says "the second-best month," decide up front what that means when first place is tied.
A missing value. A row with NULL in the column you sort by also gets a place. In a sandbox run with four rows, one of them NULL, RANK() OVER (ORDER BY x DESC) put it in 4th, last: in DuckDB, NULLs go last in descending order too. If you don't want a row with no data competing, filter it out first with WHERE x IS NOT NULL.
A tie you don't see. ROW_NUMBER() without a tiebreaker doesn't tell you there was a tie: it just picks one. If a ranking goes into a report someone will read, check with RANK() for ties at the cutoff, even if you end up using ROW_NUMBER().
The oldest interview question with ties is "what's the second-highest value?", the second-highest-salary question, now per group. With DENSE_RANK() = 2 you get the second distinct value, even if several rows share first place. For Norte, the run gave November, with 156,600 pesos, as rango_denso 2. Another common version: "the three best-selling products in each category, including ties." There, the word "including" decides between RANK() and ROW_NUMBER(). If someone asks you for a top N with ties and you're not sure which function to pick, tell your Maestro about your case.
When you hand over a top 10, note next to the result what you did with ties. "Top 10, ties included (12 rows)" heads off the question of why there are 12, and "top 10 by sales, ties broken by date" keeps anyone from thinking tenth place is a clean cut.
Tip: Why fila breaks ties with mes
Without mes in the ORDER BY of ROW_NUMBER(), nothing decides which of the two tied months is 9 and which is 10. RANK() and DENSE_RANK() don't need it: tied rows get the same number.