Skip to content
Start free

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

ColumnType
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

sucursalmesventas
Centro2025-01-01165100
Centro2025-02-01155700
Centro2025-03-01178700

Every run starts from fresh copies of the course's tables, so try anything.

Ready to run your query.

Run it yourself

These are the rows around the tie, exactly as the solution returned them in Maestro's sandbox:

mesventasfilarangorango_denso
2025-04-01137900777
2025-10-01137200888
2025-03-01132700999
2025-07-011327001099
2025-01-01128900111110
2025-02-01127300121211

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 the ORDER 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.

Synthetic course data (CC0). The two tied months are highlighted.

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.

Check yourself: In Norte, March and July tie for 9th place with DENSE_RANK. What number does DENSE_RANK give January, the next month?

Check yourself: You want to reward the months that finished in the top 3 places and, if there's a tie for third, reward every tied month. Which function do you use to number them?