Window Functions in SQL Server: OVER and PARTITION BY
A window function calculates over a "window" of rows related to the current row, WITHOUT collapsing rows like GROUP BY does.
function() OVER ( [PARTITION BY ...] [ORDER BY ...] [ROWS/RANGE frame] )
Families:
- Aggregate : SUM, AVG, COUNT, MIN, MAX (this file)
- Ranking : ROW_NUMBER, RANK, DENSE_RANK, NTILE (file 09)
- Offset : LAG, LEAD, FIRST_VALUE, LAST_VALUE (file 10)
- Frames : running totals, moving averages (file 11)
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
Part A: GROUP BY vs Window Function
GROUP BY: one row per department (employee details are lost)
SELECT DepartmentID, AVG(Salary) AS DeptAvg
FROM dbo.Employees
GROUP BY DepartmentID;
Output
| DepartmentID | DeptAvg |
|---|---|
| NULL | 20000.000000 |
| 1 | 250000.000000 |
| 2 | 83333.333333 |
| 3 | 67333.333333 |
| 4 | 94000.000000 |
| 5 | 82500.000000 |
| 6 | 84000.000000 |
| 7 | 72500.000000 |
Window: every employee row stays, department average is added
SELECT EmployeeID, FirstName, DepartmentID, Salary,
AVG(Salary) OVER (PARTITION BY DepartmentID) AS DeptAvg
FROM dbo.Employees
ORDER BY DepartmentID;
Output
| EmployeeID | FirstName | DepartmentID | Salary | DeptAvg |
|---|---|---|---|---|
| 24 | Naveen | NULL | 20000.00 | 20000.000000 |
| 1 | Rajesh | 1 | 250000.00 | 250000.000000 |
| 2 | Anita | 2 | 180000.00 | 83333.333333 |
| 7 | Arjun | 2 | 100000.00 | 83333.333333 |
| 8 | Pooja | 2 | 55000.00 | 83333.333333 |
| 9 | Rahul | 2 | 52000.00 | 83333.333333 |
| 10 | Sneha | 2 | 58000.00 | 83333.333333 |
| 23 | Ritu | 2 | 55000.00 | 83333.333333 |
| 6 | Kavya | 3 | 110000.00 | 67333.333333 |
| 19 | Rohit | 3 | 50000.00 | 67333.333333 |
| 20 | Swati | 3 | 42000.00 | 67333.333333 |
| 3 | Vikram | 4 | 190000.00 | 94000.000000 |
| 11 | Kiran | 4 | 95000.00 | 94000.000000 |
| 12 | Divya | 4 | 70000.00 | 94000.000000 |
| 13 | Amit | 4 | 68000.00 | 94000.000000 |
... showing 15 of 25 rows
Part B: OVER() with empty brackets = whole result is one window
SELECT FirstName, Salary,
SUM(Salary) OVER () AS TotalPayroll,
AVG(Salary) OVER () AS CompanyAvg,
COUNT(*) OVER () AS TotalEmployees,
CAST(100.0 * Salary / SUM(Salary) OVER () AS DECIMAL(5,2)) AS PctOfPayroll
FROM dbo.Employees
ORDER BY Salary DESC;
Output
| FirstName | Salary | TotalPayroll | CompanyAvg | TotalEmployees | PctOfPayroll |
|---|---|---|---|---|---|
| Rajesh | 250000.00 | 2192000.00 | 87680.000000 | 25 | 11.41 |
| Vikram | 190000.00 | 2192000.00 | 87680.000000 | 25 | 8.67 |
| Anita | 180000.00 | 2192000.00 | 87680.000000 | 25 | 8.21 |
| Suresh | 130000.00 | 2192000.00 | 87680.000000 | 25 | 5.93 |
| Meena | 120000.00 | 2192000.00 | 87680.000000 | 25 | 5.47 |
| Kavya | 110000.00 | 2192000.00 | 87680.000000 | 25 | 5.02 |
| Deepak | 105000.00 | 2192000.00 | 87680.000000 | 25 | 4.79 |
| Arjun | 100000.00 | 2192000.00 | 87680.000000 | 25 | 4.56 |
| Kiran | 95000.00 | 2192000.00 | 87680.000000 | 25 | 4.33 |
| Sanjay | 90000.00 | 2192000.00 | 87680.000000 | 25 | 4.11 |
| Neha | 75000.00 | 2192000.00 | 87680.000000 | 25 | 3.42 |
| Divya | 70000.00 | 2192000.00 | 87680.000000 | 25 | 3.19 |
| Geeta | 70000.00 | 2192000.00 | 87680.000000 | 25 | 3.19 |
| Amit | 68000.00 | 2192000.00 | 87680.000000 | 25 | 3.10 |
| Lakshmi | 62000.00 | 2192000.00 | 87680.000000 | 25 | 2.83 |
... showing 15 of 25 rows
Part C: PARTITION BY (a separate window per group)
Department stats on every row
SELECT d.DepartmentName, e.FirstName, e.Salary,
COUNT(*) OVER (PARTITION BY e.DepartmentID) AS DeptHeadcount,
MIN(e.Salary) OVER (PARTITION BY e.DepartmentID) AS DeptMinSalary,
MAX(e.Salary) OVER (PARTITION BY e.DepartmentID) AS DeptMaxSalary,
e.Salary - AVG(e.Salary) OVER (PARTITION BY e.DepartmentID) AS DiffFromDeptAvg
FROM dbo.Employees e
JOIN dbo.Departments d ON d.DepartmentID = e.DepartmentID
ORDER BY d.DepartmentName, e.Salary DESC;
Output
| DepartmentName | FirstName | Salary | DeptHeadcount | DeptMinSalary | DeptMaxSalary | DiffFromDeptAvg |
|---|---|---|---|---|---|---|
| Executive | Rajesh | 250000.00 | 1 | 250000.00 | 250000.00 | 0.000000 |
| Finance | Suresh | 130000.00 | 3 | 60000.00 | 130000.00 | 46000.000000 |
| Finance | Lakshmi | 62000.00 | 3 | 60000.00 | 130000.00 | -22000.000000 |
| Finance | Manoj | 60000.00 | 3 | 60000.00 | 130000.00 | -24000.000000 |
| HR | Meena | 120000.00 | 2 | 45000.00 | 120000.00 | 37500.000000 |
| HR | Priya | 45000.00 | 2 | 45000.00 | 120000.00 | -37500.000000 |
| IT | Vikram | 190000.00 | 7 | 68000.00 | 190000.00 | 96000.000000 |
| IT | Kiran | 95000.00 | 7 | 68000.00 | 190000.00 | 1000.000000 |
| IT | Sanjay | 90000.00 | 7 | 68000.00 | 190000.00 | -4000.000000 |
| IT | Neha | 75000.00 | 7 | 68000.00 | 190000.00 | -19000.000000 |
| IT | Geeta | 70000.00 | 7 | 68000.00 | 190000.00 | -24000.000000 |
| IT | Divya | 70000.00 | 7 | 68000.00 | 190000.00 | -24000.000000 |
| IT | Amit | 68000.00 | 7 | 68000.00 | 190000.00 | -26000.000000 |
| Marketing | Kavya | 110000.00 | 3 | 42000.00 | 110000.00 | 42666.666667 |
| Marketing | Rohit | 50000.00 | 3 | 42000.00 | 110000.00 | -17333.333333 |
... showing 15 of 24 rows
Each product's share of its category revenue
WITH ProductRevenue AS
(
SELECT p.CategoryID, p.ProductName, SUM(oi.LineTotal) AS Revenue
FROM dbo.OrderItems oi JOIN dbo.Products p ON p.ProductID = oi.ProductID
GROUP BY p.CategoryID, p.ProductName
)
SELECT c.CategoryName, pr.ProductName, pr.Revenue,
SUM(pr.Revenue) OVER (PARTITION BY pr.CategoryID) AS CategoryRevenue,
CAST(100.0 * pr.Revenue / SUM(pr.Revenue) OVER (PARTITION BY pr.CategoryID) AS DECIMAL(5,2)) AS PctOfCategory,
CAST(100.0 * pr.Revenue / SUM(pr.Revenue) OVER () AS DECIMAL(5,2)) AS PctOfTotal
FROM ProductRevenue pr
JOIN dbo.Categories c ON c.CategoryID = pr.CategoryID
ORDER BY c.CategoryName, pr.Revenue DESC;
Output
| CategoryName | ProductName | Revenue | CategoryRevenue | PctOfCategory | PctOfTotal |
|---|---|---|---|---|---|
| Accessories | 27 inch Monitor | 510300.00 | 711285.00 | 71.74 | 4.10 |
| Accessories | Wireless Mouse | 84660.00 | 711285.00 | 11.90 | 0.68 |
| Accessories | Mechanical Keyboard | 69250.00 | 711285.00 | 9.74 | 0.56 |
| Accessories | USB-C Hub | 47075.00 | 711285.00 | 6.62 | 0.38 |
| Desks | Standing Desk | 325600.00 | 325600.00 | 100.00 | 2.62 |
| Desktops | Lenovo ThinkCentre | 1438250.00 | 2354000.00 | 61.10 | 11.57 |
| Desktops | Acer Aspire Desktop | 915750.00 | 2354000.00 | 38.90 | 7.37 |
| Laptops | MacBook Air M3 | 3408000.00 | 6174750.00 | 55.19 | 27.41 |
| Laptops | HP Pavilion 14 | 1938000.00 | 6174750.00 | 31.39 | 15.59 |
| Laptops | Dell Inspiron 15 | 828750.00 | 6174750.00 | 13.42 | 6.67 |
| Mobiles | iPhone 15 | 1774500.00 | 2467000.00 | 71.93 | 14.27 |
| Mobiles | Samsung Galaxy A55 | 355000.00 | 2467000.00 | 14.39 | 2.86 |
| Mobiles | Redmi Note 13 | 337500.00 | 2467000.00 | 13.68 | 2.71 |
| Office Chairs | Ergonomic Office Chair | 201000.00 | 372700.00 | 53.93 | 1.62 |
| Office Chairs | Basic Office Chair | 171700.00 | 372700.00 | 46.07 | 1.38 |
... showing 15 of 18 rows
Multiple partitions in the same query
SELECT SaleDate, Region, EmployeeID, TotalAmount,
SUM(TotalAmount) OVER (PARTITION BY Region) AS RegionTotal,
SUM(TotalAmount) OVER (PARTITION BY EmployeeID) AS EmployeeTotal,
SUM(TotalAmount) OVER (PARTITION BY Region, EmployeeID) AS RegionEmployeeTotal
FROM dbo.Sales
WHERE SaleDate < '2025-02-01'
ORDER BY Region, SaleDate;
Output
| SaleDate | Region | EmployeeID | TotalAmount | RegionTotal | EmployeeTotal | RegionEmployeeTotal |
|---|---|---|---|---|---|---|
| 2025-01-07 | East | 10 | 85000.00 | 995500.00 | 500850.00 | 85000.00 |
| 2025-01-14 | East | 7 | 165000.00 | 995500.00 | 826000.00 | 295000.00 |
| 2025-01-17 | East | 8 | 140000.00 | 995500.00 | 546000.00 | 230000.00 |
| 2025-01-19 | East | 8 | 90000.00 | 995500.00 | 546000.00 | 230000.00 |
| 2025-01-23 | East | 7 | 130000.00 | 995500.00 | 826000.00 | 295000.00 |
| 2025-01-27 | East | 23 | 120000.00 | 995500.00 | 591480.00 | 360000.00 |
| 2025-01-28 | East | 23 | 240000.00 | 995500.00 | 591480.00 | 360000.00 |
| 2025-01-31 | East | 9 | 25500.00 | 995500.00 | 144800.00 | 25500.00 |
| 2025-01-02 | North | 10 | 5400.00 | 453780.00 | 500850.00 | 103800.00 |
| 2025-01-05 | North | 8 | 36000.00 | 453780.00 | 546000.00 | 36000.00 |
| 2025-01-13 | North | 9 | 25500.00 | 453780.00 | 144800.00 | 73500.00 |
| 2025-01-15 | North | 10 | 8400.00 | 453780.00 | 500850.00 | 103800.00 |
| 2025-01-15 | North | 7 | 240000.00 | 453780.00 | 826000.00 | 240000.00 |
| 2025-01-24 | North | 10 | 8500.00 | 453780.00 | 500850.00 | 103800.00 |
| 2025-01-25 | North | 10 | 59500.00 | 453780.00 | 500850.00 | 103800.00 |
... showing 15 of 31 rows
Part D: ORDER BY inside OVER (makes it cumulative)
Without ORDER BY -> total for the partition
With ORDER BY -> running (cumulative) total up to the current row
SELECT SaleDate, Region, TotalAmount,
SUM(TotalAmount) OVER (PARTITION BY Region) AS RegionTotal,
SUM(TotalAmount) OVER (PARTITION BY Region ORDER BY SaleDate, SaleID
ROWS UNBOUNDED PRECEDING) AS RunningTotal
FROM dbo.Sales
WHERE Region = 'South' AND SaleDate < '2025-03-01'
ORDER BY SaleDate, SaleID;
Output
| SaleDate | Region | TotalAmount | RegionTotal | RunningTotal |
|---|---|---|---|---|
| 2025-01-02 | South | 240000.00 | 1117800.00 | 240000.00 |
| 2025-01-06 | South | 195000.00 | 1117800.00 | 435000.00 |
| 2025-01-11 | South | 6300.00 | 1117800.00 | 441300.00 |
| 2025-01-16 | South | 30000.00 | 1117800.00 | 471300.00 |
| 2025-01-22 | South | 195000.00 | 1117800.00 | 666300.00 |
| 2025-01-30 | South | 55000.00 | 1117800.00 | 721300.00 |
| 2025-02-03 | South | 165000.00 | 1117800.00 | 886300.00 |
| 2025-02-05 | South | 14000.00 | 1117800.00 | 900300.00 |
| 2025-02-19 | South | 5000.00 | 1117800.00 | 905300.00 |
| 2025-02-22 | South | 17500.00 | 1117800.00 | 922800.00 |
| 2025-02-28 | South | 195000.00 | 1117800.00 | 1117800.00 |
Part E: Filtering ON a Window Function
Window functions are NOT allowed in WHERE. Wrap them in a CTE / subquery.
-- This is an ERROR: SELECT * FROM dbo.Employees WHERE Salary > AVG(Salary) OVER (PARTITION BY DepartmentID);
WITH x AS
(
SELECT FirstName, DepartmentID, Salary,
AVG(Salary) OVER (PARTITION BY DepartmentID) AS DeptAvg
FROM dbo.Employees
)
SELECT * FROM x WHERE Salary > DeptAvg;
Output
| FirstName | DepartmentID | Salary | DeptAvg |
|---|---|---|---|
| Anita | 2 | 180000.00 | 83333.333333 |
| Arjun | 2 | 100000.00 | 83333.333333 |
| Kavya | 3 | 110000.00 | 67333.333333 |
| Vikram | 4 | 190000.00 | 94000.000000 |
| Kiran | 4 | 95000.00 | 94000.000000 |
| Meena | 5 | 120000.00 | 82500.000000 |
| Suresh | 6 | 130000.00 | 84000.000000 |
| Deepak | 7 | 105000.00 | 72500.000000 |
Part F: Logical Order of Query Processing
FROM -> WHERE -> GROUP BY -> HAVING -> SELECT (window functions here) -> ORDER BY
So window functions see rows AFTER WHERE / GROUP BY.
Window function over a GROUP BY result: monthly revenue + % of half-year
SELECT DATENAME(MONTH, MIN(SaleDate)) AS MonthName,
SUM(TotalAmount) AS MonthRevenue,
CAST(100.0 * SUM(TotalAmount) / SUM(SUM(TotalAmount)) OVER () AS DECIMAL(5,2)) AS PctOfTotal
FROM dbo.Sales
GROUP BY MONTH(SaleDate)
ORDER BY MONTH(SaleDate);
Output
| MonthName | MonthRevenue | PctOfTotal |
|---|---|---|
| January | 2609130.00 | 16.69 |
| February | 1678350.00 | 10.74 |
| March | 3100570.00 | 19.83 |
| April | 3223180.00 | 20.62 |
| May | 1898440.00 | 12.14 |
| June | 3124020.00 | 19.98 |
Practice Questions
- Show each order item with the total value of its order (no GROUP BY).
- Show each customer's orders with that customer's total order count.
- List employees whose salary is more than 1.2x their department average.
- Show each region's share (%) of the total 2025 sales.
- For each sale, show the highest single sale amount of that region.