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 CompilerResult:
| employee_name | salary | row_num | rank | dense_rank |
|---|---|---|---|---|
| Amit Verma | 260000.00 | 1 | 1 | 1 |
| Manish Tiwari | 150000.00 | 2 | 2 | 2 |
| Ritika Bose | 150000.00 | 3 | 2 | 2 |
| Deepak Yadav | 75000.00 | 4 | 4 | 3 |
(4 rows)
| Function | Ties | After a tie |
|---|---|---|
ROW_NUMBER | different numbers (arbitrary order) | continues 1, 2, 3, 4 |
RANK | same number | skips: 1, 2, 2, 4 |
DENSE_RANK | same number | no 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 CompilerResult:
| category_id | product_name | price | price_rank |
|---|---|---|---|
| 1 | Samsung 43" Smart TV | 31990.00 | 1 |
| 1 | Redmi Note 13 Pro | 23999.00 | 2 |
| 1 | boAt Airdopes 141 | 1299.00 | 3 |
| 2 | Women's Silk Saree | 8499.00 | 1 |
| 2 | Running Shoes | 2499.00 | 2 |
| 2 | Men's Cotton Kurta | 1899.00 | 3 |
| 3 | Prestige Pressure Cooker 5L | 2199.00 | 1 |
| 3 | Non-stick Tawa | 899.00 | 2 |
| 3 | Steel Water Bottle 1L | 549.00 | 3 |
(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 CompilerResult:
| category_id | product_name | units |
|---|---|---|
| 1 | boAt Airdopes 141 | 6 |
| 2 | Men's Cotton Kurta | 4 |
| 3 | Steel Water Bottle 1L | 9 |
| 4 | Herbal Face Wash | 9 |
| 5 | The Psychology of Money | 5 |
| 6 | Yoga Mat 6mm | 4 |
(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 CompilerResult:
| employee_name | salary |
|---|---|
| Amit Verma | 260000.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 CompilerResult:
| customer_id | order_id | order_date | status |
|---|---|---|---|
| 1 | 1019 | 2025-11-15 | Delivered |
| 2 | 1016 | 2025-09-25 | Delivered |
| 3 | 1021 | 2025-12-20 | Delivered |
| 4 | 1023 | 2026-01-26 | Pending |
| 5 | 1017 | 2025-10-12 | Delivered |
| 6 | 1022 | 2026-01-08 | Shipped |
(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 CompilerResult:
| product_name | price | price_quartile |
|---|---|---|
| Samsung 43" Smart TV | 31990.00 | 1 |
| Redmi Note 13 Pro | 23999.00 | 1 |
| Women's Silk Saree | 8499.00 | 1 |
| Cricket Bat (Kashmir Willow) | 3499.00 | 1 |
| Kumkumadi Face Oil | 2995.00 | 2 |
| Running Shoes | 2499.00 | 2 |
| Prestige Pressure Cooker 5L | 2199.00 | 2 |
| Men's Cotton Kurta | 1899.00 | 2 |
(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 = 1fails next to the window function; use a CTE. - ROW_NUMBER for "top N with ties". If ties matter, use
RANKorDENSE_RANKso 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 theORDER 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 CompilerResult:
| department | employee_name | salary |
|---|---|---|
| Leadership | Rajesh Khanna | 450000.00 |
| Marketing | Pooja Sinha | 180000.00 |
| Operations | Neha Joshi | 220000.00 |
| Technology | Amit Verma | 260000.00 |
(4 rows)
Key takeaways
ROW_NUMBER= unique numbers;RANK= ties share, then gaps;DENSE_RANK= ties share, no gaps.PARTITION BYranks 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.
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.
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)