24. Window Functions -- Advanced Row-Level Analytics
Level: Advanced SQL Server for real projects
- ROW_NUMBER -- unique sequential row number
- RANK, DENSE_RANK -- ranking with/without gaps
- OVER clause -- PARTITION BY and ORDER BY
- Running totals and moving averages
- Top N per group (without GROUP BY)
- LAG, LEAD -- access previous/next rows
- Row comparison and trends
- Common window function mistakes
- Best practices
GROUP BY aggregates into single row per group. Window functions compute per row while keeping row details. This article teaches advanced row-level analytics.
Why Window Functions Matter for Backend Developers
APIs need per-row analytics:
Frontend asks: "Show each exam result with:
- Student's rank within their class
- That student's previous exam marks (for trend)
- Top 3 in class (without filtering)"
v
Backend needs: Keep all rows, add ranking and comparison columns
v
Window functions: Per-row analytics without GROUP BY
v
Complete ranked list with trends
Window functions solve "analyze each row within groups" problems.
Window Function Syntax
SELECT
Column,
ROW_NUMBER() OVER (PARTITION BY GroupColumn ORDER BY SortColumn) AS Rank
FROM Table;
OVER clause parts:
- PARTITION BY: Divide into groups (like GROUP BY)
- ORDER BY: Rank order within partition
Example 1: ROW_NUMBER (unique per group)
Rank students within their class by marks:
SELECT
s.Name AS Student,
s.ClassName,
r.MarksObtained,
ROW_NUMBER() OVER (PARTITION BY s.ClassName ORDER BY r.MarksObtained DESC) AS RankInClass
FROM Student s
INNER JOIN ExamResult r ON s.StudentId = r.StudentId
ORDER BY s.ClassName, RankInClass;
Result (ranking):
Student | ClassName | MarksObtained | RankInClass
Ravi Kumar | 10-A | 92 | 1
Priya Sharma| 10-A | 85 | 2
Sneha Patel | 10-A | 78 | 3
Arjun Reddy | 10-B | 88 | 1
Kiran Rao | 10-B | 82 | 2
ROW_NUMBER: Unique number per partition (class). 1, 2, 3 per class.
Example 2: RANK vs DENSE_RANK (ties)
Ranking with tied marks:
SELECT
s.Name AS Student,
s.ClassName,
r.MarksObtained,
RANK() OVER (PARTITION BY s.ClassName ORDER BY r.MarksObtained DESC) AS RankGaps,
DENSE_RANK() OVER (PARTITION BY s.ClassName ORDER BY r.MarksObtained DESC) AS RankNoDuplicates
FROM Student s
INNER JOIN ExamResult r ON s.StudentId = r.StudentId
ORDER BY s.ClassName, RankGaps;
Result (ranking with ties):
Student | ClassName | MarksObtained | RankGaps | RankNoDuplicates
Ravi Kumar | 10-A | 92 | 1 | 1
Priya Sharma| 10-A | 92 | 1 | 1 (tied)
Sneha Patel | 10-A | 85 | 3 | 2 (RANK skips 2)
Arjun Reddy | 10-A | 78 | 4 | 3
RANK: 1, 1, 3 (gaps on tie). DENSE_RANK: 1, 1, 2 (no gaps).
Use RANK for olympiad-style (skip ranks). DENSE_RANK for continuous ranking.
Example 3: Running Total (cumulative sum)
Track cumulative fee payments per student:
SELECT
s.Name AS Student,
p.PaidOn,
p.Amount,
SUM(p.Amount) OVER (
PARTITION BY p.FeeAccountId
ORDER BY p.PaidOn
) AS CumulativeAmount
FROM FeePayment p
INNER JOIN FeeAccount f ON p.FeeAccountId = f.FeeAccountId
INNER JOIN Student s ON f.StudentId = s.StudentId
ORDER BY s.Name, p.PaidOn;
Result (running total):
Student | PaidOn | Amount | CumulativeAmount
Ravi Kumar | 2024-05-01 | 10000 | 10000
Ravi Kumar | 2024-05-15 | 5000 | 15000
Ravi Kumar | 2024-06-01 | 5000 | 20000
Priya Sharma| 2024-05-10 | 15000 | 15000
SUM() OVER with ORDER BY: Cumulative sum per student.
Example 4: LAG and LEAD (previous/next row)
Compare current exam to previous exam:
SELECT
s.Name AS Student,
e.ExamName,
r.MarksObtained,
LAG(r.MarksObtained) OVER (
PARTITION BY s.StudentId
ORDER BY e.ExamDate
) AS PreviousMarks,
LEAD(r.MarksObtained) OVER (
PARTITION BY s.StudentId
ORDER BY e.ExamDate
) AS NextMarks,
r.MarksObtained - LAG(r.MarksObtained) OVER (
PARTITION BY s.StudentId
ORDER BY e.ExamDate
) AS ChangeFromPrevious
FROM Student s
INNER JOIN ExamResult r ON s.StudentId = r.StudentId
INNER JOIN Exam e ON r.ExamId = e.ExamId
ORDER BY s.Name, e.ExamDate;
Result (trend):
Student | ExamName | MarksObtained | PreviousMarks | NextMarks | ChangeFromPrevious
Ravi Kumar | Midterm | 80 | NULL | 92 | NULL
Ravi Kumar | Final | 92 | 80 | NULL | +12
Priya Sharma| Midterm | 75 | NULL | 85 | NULL
Priya Sharma| Final | 85 | 75 | NULL | +10
LAG: Previous row value. LEAD: Next row value. NULL at boundaries.
Example 5: Top 3 per Subject (without GROUP BY)
Get top 3 scorers per subject:
SELECT
sub.Name AS Subject,
s.Name AS Student,
r.MarksObtained,
RANK() OVER (
PARTITION BY sub.SubjectId
ORDER BY r.MarksObtained DESC
) AS SubjectRank
FROM Subject sub
INNER JOIN Exam e ON sub.SubjectId = e.SubjectId
INNER JOIN ExamResult r ON e.ExamId = r.ExamId
INNER JOIN Student s ON r.StudentId = s.StudentId
ORDER BY sub.Name, SubjectRank
OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY;
Actually to filter top 3 per subject, use CTE:
WITH RankedResults AS (
SELECT
sub.Name AS Subject,
s.Name AS Student,
r.MarksObtained,
RANK() OVER (
PARTITION BY sub.SubjectId
ORDER BY r.MarksObtained DESC
) AS SubjectRank
FROM Subject sub
INNER JOIN Exam e ON sub.SubjectId = e.SubjectId
INNER JOIN ExamResult r ON e.ExamId = r.ExamId
INNER JOIN Student s ON r.StudentId = s.StudentId
)
SELECT * FROM RankedResults
WHERE SubjectRank <= 3
ORDER BY Subject, SubjectRank;
Result (top 3 per subject):
Subject | Student | MarksObtained | SubjectRank
English | Priya Sharma| 95 | 1
English | Arjun Reddy | 92 | 2
English | Ravi Kumar | 88 | 3
Maths | Ravi Kumar | 98 | 1
Maths | Arjun Reddy | 95 | 2
Maths | Kiran Rao | 92 | 3
Window function in CTE. Filter WHERE rank <= 3.
Example 6: Average within Partition
Class average vs student performance:
SELECT
s.Name AS Student,
s.ClassName,
r.MarksObtained,
AVG(r.MarksObtained) OVER (
PARTITION BY s.ClassName
) AS ClassAverage,
r.MarksObtained - AVG(r.MarksObtained) OVER (
PARTITION BY s.ClassName
) AS AboveClassAverage
FROM Student s
INNER JOIN ExamResult r ON s.StudentId = r.StudentId
ORDER BY s.ClassName, r.MarksObtained DESC;
Result (comparison):
Student | ClassName | MarksObtained | ClassAverage | AboveClassAverage
Ravi Kumar | 10-A | 92 | 85.0 | +7.0
Priya Sharma| 10-A | 85 | 85.0 | 0.0
Sneha Patel | 10-A | 78 | 85.0 | -7.0
Arjun Reddy | 10-B | 88 | 82.5 | +5.5
AVG() OVER: Class average per student. Allows comparison.
Common Window Function Mistakes
Mistake 1: Missing PARTITION BY (ranks entire dataset)
Wrong:
SELECT
s.Name,
s.ClassName,
r.MarksObtained,
ROW_NUMBER() OVER (ORDER BY r.MarksObtained DESC) AS Rank
FROM Student s
INNER JOIN ExamResult r ON s.StudentId = r.StudentId;
-- No PARTITION BY: Ranks globally (10-A and 10-B mixed)
Result: Top 3 across all classes, not per class.
Fix: Add PARTITION BY:
ROW_NUMBER() OVER (PARTITION BY s.ClassName ORDER BY r.MarksObtained DESC) AS Rank
Mistake 2: Window function in WHERE (invalid)
Wrong:
SELECT * FROM Student s
WHERE ROW_NUMBER() OVER (ORDER BY s.Name) = 1;
-- ERROR: Window functions not allowed in WHERE
Fix: Use CTE or subquery:
WITH Numbered AS (
SELECT *, ROW_NUMBER() OVER (ORDER BY s.Name) AS Rn
FROM Student s
)
SELECT * FROM Numbered WHERE Rn = 1;
Mistake 3: LAG/LEAD with wrong PARTITION
Wrong:
SELECT
s.Name,
r.MarksObtained,
LAG(r.MarksObtained) OVER (ORDER BY e.ExamDate) AS PrevMarks
FROM Student s
INNER JOIN ExamResult r ON s.StudentId = r.StudentId
-- No PARTITION BY: Compares across students!
-- Ravi's exam compared to Priya's previous exam (wrong)
Result: Meaningless comparisons.
Fix: Partition by student:
LAG(r.MarksObtained) OVER (PARTITION BY s.StudentId ORDER BY e.ExamDate)
Mistake 4: Confusing RANK and ROW_NUMBER with ties
Wrong:
-- Want unique rank per row
SELECT
s.Name,
RANK() OVER (ORDER BY r.MarksObtained DESC) AS Rank
FROM Student s
INNER JOIN ExamResult r ON s.StudentId = r.StudentId;
-- RANK: 1, 1, 3 (not 1, 2, 3)
Result: Gaps on tied scores.
Fix: Use ROW_NUMBER for unique:
ROW_NUMBER() OVER (ORDER BY r.MarksObtained DESC) AS Rank
Best Practices for Window Functions
- Always include ORDER BY in OVER -- Determines window frame
- Partition before ranking -- Usually PARTITION BY logical grouping
- Use CTE for complex ranking filters -- Window functions can't go in WHERE
- PARTITION BY same column as GROUP BY -- Consistent grouping
- Test ties with RANK vs DENSE_RANK -- Know the difference
- LAG/LEAD include PARTITION BY -- Compare within group, not across
- Running totals need ORDER BY -- Determines cumulative order
ROW_NUMBER: Unique per row (gaps on any tie).
1, 2, 3, 4
RANK: Rank with gaps on ties.
1, 1, 3, 4 (skip rank 2 on tie)
DENSE_RANK: Rank without gaps.
1, 1, 2, 3
Use ROW_NUMBER for pagination. RANK for olympiad-style. DENSE_RANK for continuous ranking.
Window function + CTE:
WITH Ranked AS (
SELECT
s.Name, s.ClassName, r.MarksObtained,
ROW_NUMBER() OVER (PARTITION BY s.ClassName ORDER BY r.MarksObtained DESC) AS Rank
FROM Student s
INNER JOIN ExamResult r ON s.StudentId = r.StudentId
)
SELECT * FROM Ranked WHERE Rank <= 3;
Window function in CTE. Filter Rank in outer query.
Can't use ROW_NUMBER directly in WHERE.
SUM() with ORDER BY in window:
SELECT
StudentId, PaidOn, Amount,
SUM(Amount) OVER (PARTITION BY StudentId ORDER BY PaidOn) AS CumulativeAmount
FROM FeePayment
ORDER BY StudentId, PaidOn;
ORDER BY creates window frame: all rows up to current = cumulative sum.
Without ORDER BY: Window includes all rows (total, not running).
LAG: Previous row value.
LAG(Amount) OVER (ORDER BY Date) AS PreviousAmount
LEAD: Next row value.
LEAD(Amount) OVER (ORDER BY Date) AS NextAmount
Both NULL at boundaries (no previous for first, no next for last).
No:
SELECT ClassName, AVG(MarksObtained)
FROM Student s
GROUP BY ClassName; -- Simple aggregate
Use window to compare to group average:
SELECT
s.Name, s.ClassName, r.MarksObtained,
AVG(r.MarksObtained) OVER (PARTITION BY s.ClassName) AS ClassAvg
FROM Student s
INNER JOIN ExamResult r ON s.StudentId = r.StudentId;
-- Window: Per-row aggregate (class average shown on each row)
Different purposes. GROUP BY collapses rows. Window keeps all rows.
No:
WHERE ROW_NUMBER() OVER (...) = 1; -- ERROR
Use CTE or subquery:
WITH Ranked AS (
SELECT *, ROW_NUMBER() OVER (ORDER BY Name) AS Rn
FROM Student
)
SELECT * FROM Ranked WHERE Rn = 1;
Window functions evaluated after WHERE. Use CTE to apply window first, then filter.
Use ChatGPT, Claude, or Copilot to go deeper on Window functions for ranking and analytics in SQL Server. Try these prompts:
"Show ROW_NUMBER: rank students within their class by marks""What's the difference between RANK and DENSE_RANK on tied scores?""How do I create a running total of fee payments per student?""How do I use LAG to compare exam marks to previous exam?""Quiz me: ask 5 questions about window functions, OVER, LAG/LEAD"
💡 Tip: After reading this article, paste your own code into AI and ask "What could go wrong here and why?" — fastest way to find edge cases and deepen understanding.
Next Article
- CTEs (Common Table Expressions) -- Coming soon