Skip to content

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
ParticipantEventScoreRowNumRankNoDenseRankNoQuartile
AaravQuiz951111
DiyaQuiz902221
IshaanQuiz903222
KiaraQuiz854432
VihaanQuiz855433
AnayaQuiz806643
ReyanshQuiz707754

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
EventParticipantScoreRowNumRankNoDenseRankNoFairRowNum
CodingMyra991111
CodingIshaan922222
CodingDiya923223
CodingAarav884434
CodingAnaya755545
CodingKiara756546
QuizAarav951111
QuizIshaan902222
QuizDiya903223
QuizKiara854434
QuizVihaan855435
QuizAnaya806646
QuizReyansh707757

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
FirstNameSalaryRowNumRankNoDenseRankNo
Rajesh250000.00111
Vikram190000.00222
Anita180000.00333
Suresh130000.00444
Meena120000.00555
Kavya110000.00666
Deepak105000.00777
Arjun100000.00888
Kiran95000.00999
Sanjay90000.00101010
Neha75000.00111111
Divya70000.00121212
Geeta70000.00131212
Amit68000.00141413
Lakshmi62000.00151514
... 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
DepartmentIDFirstNameSalary
1Rajesh250000.00
2Anita180000.00
3Kavya110000.00
4Vikram190000.00
5Meena120000.00
6Suresh130000.00
7Deepak105000.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
DepartmentIDFirstNameSalarydr
NULLNaveen20000.001
1Rajesh250000.001
2Anita180000.001
2Arjun100000.002
3Kavya110000.001
3Rohit50000.002
4Vikram190000.001
4Kiran95000.002
5Meena120000.001
5Priya45000.002
6Suresh130000.001
6Lakshmi62000.002
7Deepak105000.001
7Farhan40000.002

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
SegmentCustomerNameRevenuernk
CorporatePune Tech Park1649975.001
CorporateMarina Digital1326609.002
CorporateInfosoft Solutions1163225.003
RetailLakshmi Stores1925592.501
RetailKerala Kart644925.002
RetailShivaji Stationers548650.003
SMBRavi Traders1527910.001
SMBAwadh Enterprises673885.002
SMBPink City Furnishers536100.003

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
CustomerIDOrderIDOrderDateStatus
1752025-05-21Delivered
2582025-01-26Delivered
3792025-06-15Delivered
4692025-04-12Shipped
5802025-06-24Delivered
6602025-02-09Delivered
7362024-09-01Delivered
8282024-07-09Delivered
9402024-09-28Delivered
10612025-02-17Delivered
11552025-01-06Delivered
12492024-11-26Delivered
13722025-05-02Shipped
14322024-08-06Delivered
15782025-06-10Delivered
... 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
OrderIDCustomerIDOrderDatern
7032025-04-1711
6942025-04-1212
6842025-04-0213
6732025-03-2814
6652025-03-2215
65172025-03-1616
6412025-03-0817
63152025-03-0218
6242025-02-2219
61102025-02-1720

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
OrderIDCustomerIDOrderDate
7032025-04-17
6942025-04-12
6842025-04-02
6732025-03-28
6652025-03-22
65172025-03-16
6412025-03-08
63152025-03-02
6242025-02-22
61102025-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
CustomerIDOrderIDOrderDateOrderSequence
112024-01-091
1102024-03-122
1132024-04-023
1262024-06-284
1392024-09-235
1462024-11-066
1522024-12-187
1642025-03-088
1712025-04-259
1732025-05-0910
1752025-05-2111
2142024-04-051
2202024-05-172
2212024-05-223
2352024-08-274
... 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
StreakStartStreakEndDaysInStreak
2025-03-022025-03-032
2025-03-062025-03-061
2025-03-082025-03-147
2025-03-162025-03-205
2025-03-222025-03-221
2025-03-242025-03-252
2025-03-272025-03-315

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
CustomerNameRevenueQuartileBand
Lakshmi Stores1925592.501Platinum
Pune Tech Park1649975.001Platinum
Ravi Traders1527910.001Platinum
Marina Digital1326609.001Platinum
Infosoft Solutions1163225.001Platinum
Awadh Enterprises673885.002Gold
Kerala Kart644925.002Gold
Silicon Valley Pvt Ltd574857.002Gold
Shivaji Stationers548650.002Gold
Pink City Furnishers536100.003Silver
Gateway Retail476850.003Silver
Mumbai Mart365735.003Silver
Northern Distributors275200.003Silver
Bengal Book House259075.004Bronze
Deccan Systems220310.004Bronze
... 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
FirstNameSalaryPercentRankCumeDist
Naveen20000.000.0000.040
Farhan40000.000.0420.080
Swati42000.000.0830.120
Priya45000.000.1250.160
Rohit50000.000.1670.200
Rahul52000.000.2080.240
Pooja55000.000.2500.320
Ritu55000.000.2500.320
Sneha58000.000.3330.360
Manoj60000.000.3750.400
Lakshmi62000.000.4170.440
Amit68000.000.4580.480
Divya70000.000.5000.560
Geeta70000.000.5000.560
Neha75000.000.5830.600
... showing 15 of 25 rows

Practice Questions

  1. Find the 2nd highest score in each event (handle ties with DENSE_RANK).
  2. Find the top selling product (by quantity) in every month of 2025 (use Sales).
  3. Show the first order of every customer and the product categories in it.
  4. Rank sales people by 2025 revenue within each Region.
  5. Remove duplicate rows from a copy of CompetitionScores keeping the highest score per participant & event.