Ranking Functions
Need to find the top 3 products in each category? Show a leaderboard? Remove duplicate records? Ranking functions are your answer.
The Four Ranking Functions
| Function | Handles Ties | Gaps | Use Case |
|---|---|---|---|
| ROW_NUMBER() | Assigns different numbers | No | Deduplication, pagination |
| RANK() | Same rank for ties | Yes | Sports rankings |
| DENSE_RANK() | Same rank for ties | No | Medal standings |
| NTILE(n) | N/A | N/A | Quartiles, percentiles |
Visual Comparison
Consider these test scores: 95, 95, 90, 85, 85, 80
| score | ROW_NUMBER | RANK | DENSE_RANK | NTILE(3) |
|---|---|---|---|---|
| 95 | 1 | 1 | 1 | 1 |
| 95 | 2 | 1 | 1 | 1 |
| 90 | 3 | 3 | 2 | 2 |
| 85 | 4 | 4 | 3 | 2 |
| 85 | 5 | 4 | 3 | 3 |
| 80 | 6 | 6 | 4 | 3 |
Notice: ROW_NUMBER always has unique values, RANK skips numbers (1,1,3), DENSE_RANK doesn't (1,1,2).
ROW_NUMBER(): Unique Sequential Numbers
Basic Syntax
SELECT
name,
department,
salary,
ROW_NUMBER() OVER (ORDER BY salary DESC) as rank
FROM employees;
Deduplication: Keep Most Recent
-- Find duplicate emails, keep only the most recent user
WITH ranked AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY email
ORDER BY created_at DESC
) as rn
FROM users
)
SELECT * FROM ranked WHERE rn = 1;
ROW_NUMBER guarantees exactly one row gets number 1 per partition. RANK or DENSE_RANK could give ties, returning multiple rows.
Pagination
-- Get page 3 (rows 21-30) of products
WITH numbered AS (
SELECT *,
ROW_NUMBER() OVER (ORDER BY name) as row_num
FROM products
)
SELECT * FROM numbered
WHERE row_num BETWEEN 21 AND 30;
RANK(): Gaps After Ties
SELECT
name,
sales,
RANK() OVER (ORDER BY sales DESC) as sales_rank
FROM salespeople;
| name | sales | sales_rank |
|---|---|---|
| Alice | 50000 | 1 |
| Bob | 45000 | 2 |
| Carol | 45000 | 2 |
| Dave | 40000 | 4 |
Notice: Bob and Carol tie at rank 2, Dave is rank 4 (not 3). Like Olympic rankings!
DENSE_RANK(): No Gaps
SELECT
name,
sales,
DENSE_RANK() OVER (ORDER BY sales DESC) as sales_rank
FROM salespeople;
| name | sales | sales_rank |
|---|---|---|
| Alice | 50000 | 1 |
| Bob | 45000 | 2 |
| Carol | 45000 | 2 |
| Dave | 40000 | 3 |
Now Dave is rank 3. No gaps!
NTILE(n): Divide into Buckets
NTILE divides rows into n roughly equal groups.
SELECT
name,
salary,
NTILE(4) OVER (ORDER BY salary DESC) as quartile
FROM employees;
| name | salary | quartile | interpretation |
|---|---|---|---|
| Alice | 150000 | 1 | Top 25% |
| Bob | 120000 | 1 | Top 25% |
| Carol | 100000 | 2 | 25-50% |
| Dave | 80000 | 2 | 25-50% |
| Eve | 70000 | 3 | 50-75% |
| Frank | 60000 | 3 | 50-75% |
| Grace | 55000 | 4 | Bottom 25% |
| Henry | 50000 | 4 | Bottom 25% |
Use Case: Percentile Analysis
-- Find customers in top 10% by spending
SELECT *
FROM (
SELECT
customer_id,
total_spent,
NTILE(10) OVER (ORDER BY total_spent DESC) as decile
FROM customer_spending
) ranked
WHERE decile = 1;
Real-World: Top N Per Group
-- Top 3 products per category by revenue
WITH ranked_products AS (
SELECT
category,
product_name,
revenue,
ROW_NUMBER() OVER (
PARTITION BY category
ORDER BY revenue DESC
) as rank
FROM products
)
SELECT category, product_name, revenue
FROM ranked_products
WHERE rank <= 3;
| category | product_name | revenue |
|---|---|---|
| Electronics | iPhone 15 | 2500000 |
| Electronics | MacBook Pro | 1800000 |
| Electronics | AirPods Pro | 950000 |
| Clothing | Winter Jacket | 450000 |
| Clothing | Running Shoes | 380000 |
| Clothing | Jeans | 320000 |
Key Takeaways
- ROW_NUMBER - Always unique, perfect for deduplication and pagination
- RANK - Same rank for ties, gaps after (1,1,3)
- DENSE_RANK - Same rank for ties, no gaps (1,1,2)
- NTILE - Divides into equal buckets for percentiles
Next up: Aggregate Window Functions - Running totals, moving averages, and more.