ROW_NUMBER, RANK, DENSE_RANK and NTILE in SQL Server
Marks : 95, 90, 90, 85
ROW_NUMBER : 1, 2, 3, 4 -> always unique (ties broken randomly unless ORDER BY is unique)
RANK : 1, 2, 2, 4 -> ties share rank, then a GAP
DENSE_RANK : 1, 2, 2, 3 -> ties share rank, NO gap
NTILE(2) : 1, 1, 2, 2 -> split rows into N buckets
Run the sample database script first. Every query below was run on that data, and the Output under each query is the real result.
USE SqlLearningDb;
GO
Topic table: CompetitionScores (with deliberate ties)
DROP TABLE IF EXISTS dbo.CompetitionScores;
CREATE TABLE dbo.CompetitionScores
(
ParticipantID INT IDENTITY(1,1) PRIMARY KEY,
Participant VARCHAR(50) NOT NULL,
School VARCHAR(50) NOT NULL,
Event VARCHAR(20) NOT NULL,
Score INT NOT NULL,
SubmittedAt DATETIME2 NOT NULL
);
INSERT INTO dbo.CompetitionScores (Participant, School, Event, Score, SubmittedAt) VALUES
('Aarav', 'DPS', 'Quiz', 95, '2025-05-01 10:00'),
('Diya', 'DPS', 'Quiz', 90, '2025-05-01 10:05'),
('Ishaan', 'KV', 'Quiz', 90, '2025-05-01 09:58'),
('Kiara', 'KV', 'Quiz', 85, '2025-05-01 10:10'),
('Vihaan', 'Narayana', 'Quiz', 85, '2025-05-01 10:12'),
('Anaya', 'Narayana', 'Quiz', 80, '2025-05-01 10:01'),
('Reyansh', 'DPS', 'Quiz', 70, '2025-05-01 10:20'),
('Aarav', 'DPS', 'Coding', 88, '2025-05-02 11:00'),
('Diya', 'DPS', 'Coding', 92, '2025-05-02 11:30'),
('Ishaan', 'KV', 'Coding', 92, '2025-05-02 11:10'),
('Kiara', 'KV', 'Coding', 75, '2025-05-02 11:40'),
('Myra', 'Narayana', 'Coding', 99, '2025-05-02 11:50'),
('Anaya', 'Narayana', 'Coding', 75, '2025-05-02 11:05');
GO
Messages(13 rows affected)
Part A: Side by Side Comparison
SELECT Participant, Event, Score,
ROW_NUMBER() OVER (ORDER BY Score DESC) AS RowNum,
RANK() OVER (ORDER BY Score DESC) AS RankNo,
DENSE_RANK() OVER (ORDER BY Score DESC) AS DenseRankNo,
NTILE(4) OVER (ORDER BY Score DESC) AS Quartile
FROM dbo.CompetitionScores
WHERE Event = 'Quiz';
Output
| Participant | Event | Score | RowNum | RankNo | DenseRankNo | Quartile |
| Aarav | Quiz | 95 | 1 | 1 | 1 | 1 |
| Diya | Quiz | 90 | 2 | 2 | 2 | 1 |
| Ishaan | Quiz | 90 | 3 | 2 | 2 | 2 |
| Kiara | Quiz | 85 | 4 | 4 | 3 | 2 |
| Vihaan | Quiz | 85 | 5 | 4 | 3 | 3 |
| Anaya | Quiz | 80 | 6 | 6 | 4 | 3 |
| Reyansh | Quiz | 70 | 7 | 7 | 5 | 4 |
Same with PARTITION BY Event: ranking restarts for each event
SELECT Event, Participant, Score,
ROW_NUMBER() OVER (PARTITION BY Event ORDER BY Score DESC) AS RowNum,
RANK() OVER (PARTITION BY Event ORDER BY Score DESC) AS RankNo,
DENSE_RANK() OVER (PARTITION BY Event ORDER BY Score DESC) AS DenseRankNo,
-- Tie breaker : whoever submitted earlier wins
ROW_NUMBER() OVER (PARTITION BY Event ORDER BY Score DESC, SubmittedAt) AS FairRowNum
FROM dbo.CompetitionScores
ORDER BY Event, Score DESC;
Output
| Event | Participant | Score | RowNum | RankNo | DenseRankNo | FairRowNum |
| Coding | Myra | 99 | 1 | 1 | 1 | 1 |
| Coding | Ishaan | 92 | 2 | 2 | 2 | 2 |
| Coding | Diya | 92 | 3 | 2 | 2 | 3 |
| Coding | Aarav | 88 | 4 | 4 | 3 | 4 |
| Coding | Anaya | 75 | 5 | 5 | 4 | 5 |
| Coding | Kiara | 75 | 6 | 5 | 4 | 6 |
| Quiz | Aarav | 95 | 1 | 1 | 1 | 1 |
| Quiz | Ishaan | 90 | 2 | 2 | 2 | 2 |
| Quiz | Diya | 90 | 3 | 2 | 2 | 3 |
| Quiz | Kiara | 85 | 4 | 4 | 3 | 4 |
| Quiz | Vihaan | 85 | 5 | 4 | 3 | 5 |
| Quiz | Anaya | 80 | 6 | 6 | 4 | 6 |
| Quiz | Reyansh | 70 | 7 | 7 | 5 | 7 |
Employee salaries (55000, 70000 are tied in the data)
SELECT FirstName, Salary,
ROW_NUMBER() OVER (ORDER BY Salary DESC) AS RowNum,
RANK() OVER (ORDER BY Salary DESC) AS RankNo,
DENSE_RANK() OVER (ORDER BY Salary DESC) AS DenseRankNo
FROM dbo.Employees
ORDER BY Salary DESC;
Output
| FirstName | Salary | RowNum | RankNo | DenseRankNo |
| Rajesh | 250000.00 | 1 | 1 | 1 |
| Vikram | 190000.00 | 2 | 2 | 2 |
| Anita | 180000.00 | 3 | 3 | 3 |
| Suresh | 130000.00 | 4 | 4 | 4 |
| Meena | 120000.00 | 5 | 5 | 5 |
| Kavya | 110000.00 | 6 | 6 | 6 |
| Deepak | 105000.00 | 7 | 7 | 7 |
| Arjun | 100000.00 | 8 | 8 | 8 |
| Kiran | 95000.00 | 9 | 9 | 9 |
| Sanjay | 90000.00 | 10 | 10 | 10 |
| Neha | 75000.00 | 11 | 11 | 11 |
| Divya | 70000.00 | 12 | 12 | 12 |
| Geeta | 70000.00 | 13 | 12 | 12 |
| Amit | 68000.00 | 14 | 14 | 13 |
| Lakshmi | 62000.00 | 15 | 15 | 14 |
... showing 15 of 25 rows
Part B: TOP-N per Group (most common real use)
Highest paid employee per department (exactly 1 row each -> ROW_NUMBER)
WITH r AS
(
SELECT DepartmentID, FirstName, Salary,
ROW_NUMBER() OVER (PARTITION BY DepartmentID ORDER BY Salary DESC, EmployeeID) AS rn
FROM dbo.Employees
WHERE DepartmentID IS NOT NULL
)
SELECT DepartmentID, FirstName, Salary FROM r WHERE rn = 1;
Output
| DepartmentID | FirstName | Salary |
| 1 | Rajesh | 250000.00 |
| 2 | Anita | 180000.00 |
| 3 | Kavya | 110000.00 |
| 4 | Vikram | 190000.00 |
| 5 | Meena | 120000.00 |
| 6 | Suresh | 130000.00 |
| 7 | Deepak | 105000.00 |
Top 2 salaries per department Including ties -> DENSE_RANK
WITH r AS
(
SELECT DepartmentID, FirstName, Salary,
DENSE_RANK() OVER (PARTITION BY DepartmentID ORDER BY Salary DESC) AS dr
FROM dbo.Employees
)
SELECT * FROM r WHERE dr <= 2 ORDER BY DepartmentID, dr;
Output
| DepartmentID | FirstName | Salary | dr |
| NULL | Naveen | 20000.00 | 1 |
| 1 | Rajesh | 250000.00 | 1 |
| 2 | Anita | 180000.00 | 1 |
| 2 | Arjun | 100000.00 | 2 |
| 3 | Kavya | 110000.00 | 1 |
| 3 | Rohit | 50000.00 | 2 |
| 4 | Vikram | 190000.00 | 1 |
| 4 | Kiran | 95000.00 | 2 |
| 5 | Meena | 120000.00 | 1 |
| 5 | Priya | 45000.00 | 2 |
| 6 | Suresh | 130000.00 | 1 |
| 6 | Lakshmi | 62000.00 | 2 |
| 7 | Deepak | 105000.00 | 1 |
| 7 | Farhan | 40000.00 | 2 |
Nth highest salary (N = 3) - DENSE_RANK version (best answer in interviews)
WITH r AS (SELECT DISTINCT Salary, DENSE_RANK() OVER (ORDER BY Salary DESC) AS dr FROM dbo.Employees)
SELECT Salary AS ThirdHighestSalary FROM r WHERE dr = 3;
Output
| ThirdHighestSalary |
| 180000.00 |
Top 3 customers by revenue in each customer segment
WITH CustRev AS
(
SELECT c.Segment, c.CustomerName, SUM(oi.LineTotal) AS Revenue
FROM dbo.Customers c
JOIN dbo.Orders o ON o.CustomerID = c.CustomerID
JOIN dbo.OrderItems oi ON oi.OrderID = o.OrderID
GROUP BY c.Segment, c.CustomerName
),
Ranked AS
(
SELECT *, RANK() OVER (PARTITION BY Segment ORDER BY Revenue DESC) AS rnk FROM CustRev
)
SELECT * FROM Ranked WHERE rnk <= 3 ORDER BY Segment, rnk;
Output
| Segment | CustomerName | Revenue | rnk |
| Corporate | Pune Tech Park | 1649975.00 | 1 |
| Corporate | Marina Digital | 1326609.00 | 2 |
| Corporate | Infosoft Solutions | 1163225.00 | 3 |
| Retail | Lakshmi Stores | 1925592.50 | 1 |
| Retail | Kerala Kart | 644925.00 | 2 |
| Retail | Shivaji Stationers | 548650.00 | 3 |
| SMB | Ravi Traders | 1527910.00 | 1 |
| SMB | Awadh Enterprises | 673885.00 | 2 |
| SMB | Pink City Furnishers | 536100.00 | 3 |
Latest order of each customer
WITH r AS
(
SELECT o.*, ROW_NUMBER() OVER (PARTITION BY CustomerID ORDER BY OrderDate DESC, OrderID DESC) AS rn
FROM dbo.Orders o
)
SELECT CustomerID, OrderID, OrderDate, Status FROM r WHERE rn = 1;
Output
| CustomerID | OrderID | OrderDate | Status |
| 1 | 75 | 2025-05-21 | Delivered |
| 2 | 58 | 2025-01-26 | Delivered |
| 3 | 79 | 2025-06-15 | Delivered |
| 4 | 69 | 2025-04-12 | Shipped |
| 5 | 80 | 2025-06-24 | Delivered |
| 6 | 60 | 2025-02-09 | Delivered |
| 7 | 36 | 2024-09-01 | Delivered |
| 8 | 28 | 2024-07-09 | Delivered |
| 9 | 40 | 2024-09-28 | Delivered |
| 10 | 61 | 2025-02-17 | Delivered |
| 11 | 55 | 2025-01-06 | Delivered |
| 12 | 49 | 2024-11-26 | Delivered |
| 13 | 72 | 2025-05-02 | Shipped |
| 14 | 32 | 2024-08-06 | Delivered |
| 15 | 78 | 2025-06-10 | Delivered |
... showing 15 of 17 rows
Part C: Other Uses of ROW_NUMBER
Pagination: page 2, page size 10 (ROW_NUMBER way)
DECLARE @PageNo INT = 2, @PageSize INT = 10;
WITH Paged AS
(
SELECT OrderID, CustomerID, OrderDate,
ROW_NUMBER() OVER (ORDER BY OrderDate DESC, OrderID DESC) AS rn
FROM dbo.Orders
)
SELECT * FROM Paged
WHERE rn BETWEEN (@PageNo - 1) * @PageSize + 1 AND @PageNo * @PageSize;
Output
| OrderID | CustomerID | OrderDate | rn |
| 70 | 3 | 2025-04-17 | 11 |
| 69 | 4 | 2025-04-12 | 12 |
| 68 | 4 | 2025-04-02 | 13 |
| 67 | 3 | 2025-03-28 | 14 |
| 66 | 5 | 2025-03-22 | 15 |
| 65 | 17 | 2025-03-16 | 16 |
| 64 | 1 | 2025-03-08 | 17 |
| 63 | 15 | 2025-03-02 | 18 |
| 62 | 4 | 2025-02-22 | 19 |
| 61 | 10 | 2025-02-17 | 20 |
Same with OFFSET / FETCH (modern way)
SELECT OrderID, CustomerID, OrderDate
FROM dbo.Orders
ORDER BY OrderDate DESC, OrderID DESC
OFFSET (@PageNo - 1) * @PageSize ROWS FETCH NEXT @PageSize ROWS ONLY;
Output
| OrderID | CustomerID | OrderDate |
| 70 | 3 | 2025-04-17 |
| 69 | 4 | 2025-04-12 |
| 68 | 4 | 2025-04-02 |
| 67 | 3 | 2025-03-28 |
| 66 | 5 | 2025-03-22 |
| 65 | 17 | 2025-03-16 |
| 64 | 1 | 2025-03-08 |
| 63 | 15 | 2025-03-02 |
| 62 | 4 | 2025-02-22 |
| 61 | 10 | 2025-02-17 |
Number the orders of each customer: 1st order, 2nd order, ...
SELECT CustomerID, OrderID, OrderDate,
ROW_NUMBER() OVER (PARTITION BY CustomerID ORDER BY OrderDate, OrderID) AS OrderSequence
FROM dbo.Orders
ORDER BY CustomerID, OrderSequence;
Output
| CustomerID | OrderID | OrderDate | OrderSequence |
| 1 | 1 | 2024-01-09 | 1 |
| 1 | 10 | 2024-03-12 | 2 |
| 1 | 13 | 2024-04-02 | 3 |
| 1 | 26 | 2024-06-28 | 4 |
| 1 | 39 | 2024-09-23 | 5 |
| 1 | 46 | 2024-11-06 | 6 |
| 1 | 52 | 2024-12-18 | 7 |
| 1 | 64 | 2025-03-08 | 8 |
| 1 | 71 | 2025-04-25 | 9 |
| 1 | 73 | 2025-05-09 | 10 |
| 1 | 75 | 2025-05-21 | 11 |
| 2 | 14 | 2024-04-05 | 1 |
| 2 | 20 | 2024-05-17 | 2 |
| 2 | 21 | 2024-05-22 | 3 |
| 2 | 35 | 2024-08-27 | 4 |
... showing 15 of 80 rows
Gaps and islands: find streaks of consecutive sale days in March 2025
WITH Days AS (SELECT DISTINCT SaleDate FROM dbo.Sales WHERE SaleDate >= '2025-03-01' AND SaleDate < '2025-04-01'),
Grp AS (SELECT SaleDate,
DATEADD(DAY, -ROW_NUMBER() OVER (ORDER BY SaleDate), SaleDate) AS GroupKey
FROM Days)
SELECT MIN(SaleDate) AS StreakStart, MAX(SaleDate) AS StreakEnd, COUNT(*) AS DaysInStreak
FROM Grp
GROUP BY GroupKey
ORDER BY StreakStart;
Output
| StreakStart | StreakEnd | DaysInStreak |
| 2025-03-02 | 2025-03-03 | 2 |
| 2025-03-06 | 2025-03-06 | 1 |
| 2025-03-08 | 2025-03-14 | 7 |
| 2025-03-16 | 2025-03-20 | 5 |
| 2025-03-22 | 2025-03-22 | 1 |
| 2025-03-24 | 2025-03-25 | 2 |
| 2025-03-27 | 2025-03-31 | 5 |
Part D: NTILE - split into buckets
Divide customers into 4 revenue quartiles
WITH CustRev AS
(
SELECT o.CustomerID, SUM(oi.LineTotal) AS Revenue
FROM dbo.Orders o JOIN dbo.OrderItems oi ON oi.OrderID = o.OrderID
GROUP BY o.CustomerID
)
SELECT c.CustomerName, cr.Revenue,
NTILE(4) OVER (ORDER BY cr.Revenue DESC) AS Quartile,
CASE NTILE(4) OVER (ORDER BY cr.Revenue DESC)
WHEN 1 THEN 'Platinum' WHEN 2 THEN 'Gold' WHEN 3 THEN 'Silver' ELSE 'Bronze' END AS Band
FROM CustRev cr JOIN dbo.Customers c ON c.CustomerID = cr.CustomerID;
Output
| CustomerName | Revenue | Quartile | Band |
| Lakshmi Stores | 1925592.50 | 1 | Platinum |
| Pune Tech Park | 1649975.00 | 1 | Platinum |
| Ravi Traders | 1527910.00 | 1 | Platinum |
| Marina Digital | 1326609.00 | 1 | Platinum |
| Infosoft Solutions | 1163225.00 | 1 | Platinum |
| Awadh Enterprises | 673885.00 | 2 | Gold |
| Kerala Kart | 644925.00 | 2 | Gold |
| Silicon Valley Pvt Ltd | 574857.00 | 2 | Gold |
| Shivaji Stationers | 548650.00 | 2 | Gold |
| Pink City Furnishers | 536100.00 | 3 | Silver |
| Gateway Retail | 476850.00 | 3 | Silver |
| Mumbai Mart | 365735.00 | 3 | Silver |
| Northern Distributors | 275200.00 | 3 | Silver |
| Bengal Book House | 259075.00 | 4 | Bronze |
| Deccan Systems | 220310.00 | 4 | Bronze |
... showing 15 of 17 rows
PERCENT_RANK and CUME_DIST (relative position 0..1)
SELECT FirstName, Salary,
CAST(PERCENT_RANK() OVER (ORDER BY Salary) AS DECIMAL(5,3)) AS PercentRank,
CAST(CUME_DIST() OVER (ORDER BY Salary) AS DECIMAL(5,3)) AS CumeDist
FROM dbo.Employees;
Output
| FirstName | Salary | PercentRank | CumeDist |
| Naveen | 20000.00 | 0.000 | 0.040 |
| Farhan | 40000.00 | 0.042 | 0.080 |
| Swati | 42000.00 | 0.083 | 0.120 |
| Priya | 45000.00 | 0.125 | 0.160 |
| Rohit | 50000.00 | 0.167 | 0.200 |
| Rahul | 52000.00 | 0.208 | 0.240 |
| Pooja | 55000.00 | 0.250 | 0.320 |
| Ritu | 55000.00 | 0.250 | 0.320 |
| Sneha | 58000.00 | 0.333 | 0.360 |
| Manoj | 60000.00 | 0.375 | 0.400 |
| Lakshmi | 62000.00 | 0.417 | 0.440 |
| Amit | 68000.00 | 0.458 | 0.480 |
| Divya | 70000.00 | 0.500 | 0.560 |
| Geeta | 70000.00 | 0.500 | 0.560 |
| Neha | 75000.00 | 0.583 | 0.600 |
... showing 15 of 25 rows
Practice Questions
- Find the 2nd highest score in each event (handle ties with DENSE_RANK).
- Find the top selling product (by quantity) in every month of 2025 (use Sales).
- Show the first order of every customer and the product categories in it.
- Rank sales people by 2025 revenue within each Region.
- Remove duplicate rows from a copy of CompetitionScores keeping the highest score per participant & event.