All 40 lessons

Lesson 27 of 40 · Window Functions

ROW_NUMBER, RANK, DENSE_RANK and NTILE

By Skillancy Editorial Team

Every example runs on the ShopKart dataset, already loaded in the free SQL Compiler. Click Try it in the SQL Compiler under a query to open it there, then press Run.

Ranking window functions number rows in order: ROW_NUMBER gives every row a unique number, RANK and DENSE_RANK give ties the same rank, and NTILE splits rows into equal buckets. They solve some of the most common interview questions: top N per group, nth highest value and removing duplicates.

Syntax

ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)
RANK()       OVER (PARTITION BY ... ORDER BY ...)
DENSE_RANK() OVER (PARTITION BY ... ORDER BY ...)
NTILE(n)     OVER (ORDER BY ...)

ORDER BY inside OVER is required: it defines what "first" means.

How they handle ties

ShopKart's Technology team has two people on the same salary. Watch how each function treats them:

SELECT employee_name,
       salary,
       ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num,
       RANK()       OVER (ORDER BY salary DESC) AS rank,
       DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank
FROM employees
WHERE department = 'Technology';
Try it in the SQL Compiler

Result:

employee_namesalaryrow_numrankdense_rank
Amit Verma260000.00111
Manish Tiwari150000.00222
Ritika Bose150000.00322
Deepak Yadav75000.00443

(4 rows)

FunctionTiesAfter a tie
ROW_NUMBERdifferent numbers (arbitrary order)continues 1, 2, 3, 4
RANKsame numberskips: 1, 2, 2, 4
DENSE_RANKsame numberno gap: 1, 2, 2, 3

Examples

1. Rank within each group

Products ranked by price inside each category:

SELECT category_id,
       product_name,
       price,
       RANK() OVER (PARTITION BY category_id ORDER BY price DESC) AS price_rank
FROM products
WHERE category_id IN (1, 2, 3)
ORDER BY category_id, price_rank;
Try it in the SQL Compiler

Result:

category_idproduct_namepriceprice_rank
1Samsung 43" Smart TV31990.001
1Redmi Note 13 Pro23999.002
1boAt Airdopes 1411299.003
2Women's Silk Saree8499.001
2Running Shoes2499.002
2Men's Cotton Kurta1899.003
3Prestige Pressure Cooker 5L2199.001
3Non-stick Tawa899.002
3Steel Water Bottle 1L549.003

(9 rows)

PARTITION BY category_id restarts the numbering for each category.

2. Top N per group

The best-selling product (by units) in each category. Rank in a CTE, filter outside:

WITH product_units AS (
  SELECT p.category_id,
         p.product_name,
         SUM(oi.quantity) AS units
  FROM order_items oi
  JOIN products p ON p.product_id = oi.product_id
  GROUP BY p.category_id, p.product_name
),
ranked AS (
  SELECT *,
         ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY units DESC, product_name) AS rn
  FROM product_units
)
SELECT category_id, product_name, units
FROM ranked
WHERE rn = 1
ORDER BY category_id;
Try it in the SQL Compiler

Result:

category_idproduct_nameunits
1boAt Airdopes 1416
2Men's Cotton Kurta4
3Steel Water Bottle 1L9
4Herbal Face Wash9
5The Psychology of Money5
6Yoga Mat 6mm4

(6 rows)

Change rn = 1 to rn <= 3 for the top 3. Adding product_name to the ORDER BY makes ties deterministic.

3. Nth highest value

The second-highest salary at ShopKart:

WITH ranked AS (
  SELECT employee_name, salary,
         DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
  FROM employees
)
SELECT employee_name, salary
FROM ranked
WHERE rnk = 2;
Try it in the SQL Compiler

Result:

employee_namesalary
Amit Verma260000.00

(1 row)

Use DENSE_RANK here: if two people shared the top salary, RANK would have no rank 2 at all.

4. Each customer's latest order

ROW_NUMBER is the go-to for "most recent record per entity":

WITH ordered AS (
  SELECT customer_id, order_id, order_date, status,
         ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date DESC) AS rn
  FROM orders
)
SELECT customer_id, order_id, order_date, status
FROM ordered
WHERE rn = 1
ORDER BY customer_id
LIMIT 6;
Try it in the SQL Compiler

Result:

customer_idorder_idorder_datestatus
110192025-11-15Delivered
210162025-09-25Delivered
310212025-12-20Delivered
410232026-01-26Pending
510172025-10-12Delivered
610222026-01-08Shipped

(6 rows)

The same pattern removes duplicate rows: number them within each duplicate group and keep rn = 1.

5. NTILE: split into buckets

Divide products into 4 price quartiles:

SELECT product_name,
       price,
       NTILE(4) OVER (ORDER BY price DESC) AS price_quartile
FROM products
ORDER BY price DESC
LIMIT 8;
Try it in the SQL Compiler

Result:

product_namepriceprice_quartile
Samsung 43" Smart TV31990.001
Redmi Note 13 Pro23999.001
Women's Silk Saree8499.001
Cricket Bat (Kashmir Willow)3499.001
Kumkumadi Face Oil2995.002
Running Shoes2499.002
Prestige Pressure Cooker 5L2199.002
Men's Cotton Kurta1899.002

(8 rows)

With 15 products and 4 buckets, the first three buckets get 4 products and the last gets 3. NTILE is useful for customer segments like "top 25% of spenders".

Common mistakes

  • Filtering on the rank in the same query. WHERE rn = 1 fails next to the window function; use a CTE.
  • ROW_NUMBER for "top N with ties". If ties matter, use RANK or DENSE_RANK so tied rows aren't arbitrarily dropped.
  • No tie-breaker. ROW_NUMBER() OVER (ORDER BY salary) can number tied rows differently between runs. Add a unique column to the ORDER BY.

Practice question

Find the highest-paid employee in each department.

Show solution
WITH ranked AS (
  SELECT employee_name, department, salary,
         RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rnk
  FROM employees
)
SELECT department, employee_name, salary
FROM ranked
WHERE rnk = 1
ORDER BY department;
Try it in the SQL Compiler

Result:

departmentemployee_namesalary
LeadershipRajesh Khanna450000.00
MarketingPooja Sinha180000.00
OperationsNeha Joshi220000.00
TechnologyAmit Verma260000.00

(4 rows)

Key takeaways

  • ROW_NUMBER = unique numbers; RANK = ties share, then gaps; DENSE_RANK = ties share, no gaps.
  • PARTITION BY ranks within groups; filter the rank in an outer query.
  • Top N per group: rank in a CTE, then WHERE rank <= N.
  • Nth highest: DENSE_RANK() ... WHERE rnk = N.
Want every variation on one page?Window functions in SQL: the complete guide

Frequently asked questions

What is the difference between RANK and DENSE_RANK?

Both give tied rows the same rank. RANK then skips numbers (1, 1, 3), while DENSE_RANK does not (1, 1, 2). ROW_NUMBER never ties: it gives every row a unique number.

How do I get the top 3 products in each category?

Rank within each category using ROW_NUMBER() or RANK() OVER (PARTITION BY category ORDER BY sales DESC) in a CTE, then filter WHERE rank <= 3 in the outer query.

How do I find the second highest salary in SQL?

Use DENSE_RANK() OVER (ORDER BY salary DESC) in a CTE and filter on rank = 2. DENSE_RANK handles ties correctly, so two people sharing the top salary don't push the answer down.

Newsletter

Keep learning SQL

One email a week with SQL and analytics guides, new practice questions and cohort dates. No spam, unsubscribe anytime.

Prefer a one-page summary? Download the free SQL cheat sheet (PDF)