Tier 3 · Intermediate

ROW_NUMBER, RANK, DENSE_RANK in SQL

By Owen Middleton · Updated September 2026 · Examples run on PostgreSQL 17

What are Ranking Functions in SQL?

ROW_NUMBER, RANK, and DENSE_RANK each assign a number to every row based on its position in an ordering. They produce identical results when there are no ties. The difference only shows when two rows are equal on the sort column.

You're building a leaderboard for sales reps, ranked by total revenue with the top performer at position 1. Most of the time the rankings are clear: one person per position. But some months two reps finish with identical totals. Which function you use determines what the tied rows see.

Do ranking functions need ORDER BY inside OVER?

All three need ORDER BY inside OVER to mean anything. PostgreSQL will run them without one, and that is worse than an error: you get numbers handed out in whatever order the rows happened to arrive, with nothing to tell you the ranking is arbitrary. PARTITION BY is optional: omit it to rank across the entire result; include it to restart ranking within each group.

Here's all three side by side:

SELECT
  c.name AS customer_name,
  o.total_amount,
  ROW_NUMBER() OVER (ORDER BY o.total_amount DESC) AS row_num,
  RANK()       OVER (ORDER BY o.total_amount DESC) AS rnk,
  DENSE_RANK() OVER (ORDER BY o.total_amount DESC) AS dense_rnk
FROM orders o
JOIN customers c ON o.customer_id = c.id

ROW_NUMBER assigns a unique integer to every row. If two rows tie, one gets 1 and the other gets 2 — the order between them is arbitrary, but the numbers are always unique with no gaps.

RANK gives tied rows the same number, then skips ahead. Two rows tied for first both get 1. The next row gets 3, because two positions have been occupied by the tie.

DENSE_RANK also gives tied rows the same number, but doesn't skip. Two rows tied for first both get 1. The next row gets 2. The sequence is always consecutive.

How do ROW_NUMBER, RANK and DENSE_RANK differ on ties?

For scores 95, 95, 80, 75: ROW_NUMBER → 1, 2, 3, 4. RANK → 1, 1, 3, 4. DENSE_RANK → 1, 1, 2, 3.

How do you get the latest row per group in SQL?

The most useful pattern with ROW_NUMBER is the "latest row per group": the most recent order per customer, the latest status per account. Partition by the entity ID, order by timestamp descending, then filter for rn = 1 in an outer query:

SELECT * FROM (
  SELECT *,
    ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY ordered_at DESC) AS rn
  FROM orders
) ranked
WHERE rn = 1

This picks one row per customer regardless of ties. Which tied row is selected is not deterministic unless you add a tiebreaker column to the ORDER BY.

Should you use RANK or DENSE_RANK?

The one thing that trips people up: choosing the wrong function for the job.

Use ROW_NUMBER when you need a unique identifier per row — especially for the "one row per group" filter pattern. Use RANK or DENSE_RANK when you need to present standings that reflect ties. DENSE_RANK tells you how many distinct positions precede a row. RANK tells you how many rows precede it.

Check your understanding

Three rows have scores 100, 100, and 80. What values do RANK() and DENSE_RANK() assign to the third row?

Practice Ranking Functions in SQL

Practice · easy ecommerce · Brightlane

Brightlane's fulfillment dashboard assigns every order a unique position based on order value for priority processing.

Write a query to return the ID and total amount of every order, plus a unique sequential number assigned in descending order of total_amount.

Assumptions:

  • The orders table has one row per order with an id and a total_amount.
  • The position is 1 for the highest total_amount and increments by 1 for each subsequent order.
  • Every order receives a different position; if two orders share the same total_amount, they still receive consecutive numbers in some order.

Output:

  • One row per order, with columns id, total_amount, and row_num.
Schema · ecommerce5 tables? = nullable
categories
idinteger
nametext
parent_id?integer
products
idinteger
nametext
category_id?integer
pricenumeric
stock_qtyinteger
attributes?jsonb
order_items
idinteger
order_id?integer
product_id?integer
quantityinteger
unit_pricenumeric
customers
idinteger
nametext
emailtext
city?text
countrytext
created_attimestamptz
is_activeboolean
orders
idinteger
customer_id?integer
ordered_attimestamptz
statustext
total_amountnumeric

Run previews · Check grades

Write a query, then run it to see results here.

Worked solution

The full breakdown walks through the shape, each clause, why this approach beats the alternatives, and the trap to avoid.

See the full worked solution

9 Ranking Functions practice problems

Write a query to return the ID and total amount of every order, plus a unique sequential number assigned in descending order of total_amount.

easy ecommerce

Write a query to return the ID, name, and price of every product, plus the product's rank by price in descending order.

easy ecommerce

Write a query to return the ID, name, and price of every product, plus the product's tier number by price in descending order.

easy ecommerce

Write a query to return the ID, customer ID, and total amount of every order, plus a sequential number within that customer's orders, ordered by total_amount in descending order.

medium ecommerce

Write a query to return the ID, name, category ID, and price of every product, plus the product's rank within its category by price in descending order.

medium ecommerce

Write a query to return the ID, name, department ID, and hire date of every employee, plus the employee's seniority rank within their department.

medium hr

Write a query to return the ID and event count of every session, plus the session's dense rank by event_count in descending order.

medium analytics

Write a query to return the ID, name, and price of every product, plus three position values for each row: a unique sequential position (row_num), a tie-sharing position with gaps after ties (rnk), and a tie-sharing position without gaps (dense_rnk). All three positions are ordered by price in descending order. Sort the final result by price in descending order.

hard ecommerce

Write a query to return the ID, customer ID, and total amount of every order, plus the order's dense rank within that customer's orders by total_amount in descending order. Sort the final result by customer_id ascending and total_amount descending.

hard ecommerce

Start learning to practice all 9 Ranking Functions problems, with instant grading and mastery tracking.

Common questions about Ranking Functions

Can RANK leave gaps in the numbering?

Yes, and that is what separates it from DENSE_RANK. Two rows tied at the top both get one, and the next row gets three because two positions are already taken. DENSE_RANK would call that row two instead.

Does ROW_NUMBER give a stable answer when rows tie?

Not on its own. Tied rows get different numbers, but which one is first is arbitrary and can change between runs. Add a tiebreaker column to the ORDER BY when the answer has to be reproducible, which it does whenever you filter for the first row per group.

Can you rank inside groups rather than across the whole table?

Yes, with PARTITION BY. The numbering restarts at one for every group, which is how you rank products within each category instead of against the entire catalogue.

How you actually get good at SQL

Reading explains SQL. Writing it, over and over with instant feedback, is what makes you fluent.

That's the whole SQLMaxx loop: 600+ real problems, instant AI feedback, mastery you can actually see, and spaced review that won't let you forget.

A stack of SQL practice problem cards, the top card showing an employees table.
615 problems · 66 concepts

Real problems. Not toy examples.

615 hand-built problems spanning all 66 concepts, from basic SELECTs to window functions, built on real schemas and real business questions, the kind you'll actually get asked on the job. Enough reps to make SQL automatic.

A retro computer showing a SQL query marked correct with a green checkmark.
Instant AI feedback

Write a query. Know if it's right in one second.

No copying an answer and hoping it clicked. The AI grader checks your real query against real data, catches exactly what's wrong, and explains the fix in plain English, like a senior analyst reading over your shoulder on every problem.

A circular mastery progress dial filling from blue to green, the SQLMaxx diamond at its center.
Mastery tracking

Stop guessing whether you actually know it.

SQLMaxx tracks every concept and shows you what you've mastered and what's still shaky. Your skills fill in one concept at a time, so 'I think I get joins' becomes something you can prove.

A SQL query editor circled by a blue return arrow with a clock, scheduled to come back for review.
Spaced review

Learn it once. Keep it for good.

Most of what you learn this week fades by next week. So when a concept comes due for review, SQLMaxx hands you a fresh problem to solve from a blank editor, not a flashcard to re-read. A research-backed spaced-repetition algorithm (FSRS) times each return for right before you'd forget, so your SQL is still there months later, when the interview or the job actually needs it.