Skip to content

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
DepartmentIDDeptAvg
NULL20000.000000
1250000.000000
283333.333333
367333.333333
494000.000000
582500.000000
684000.000000
772500.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
EmployeeIDFirstNameDepartmentIDSalaryDeptAvg
24NaveenNULL20000.0020000.000000
1Rajesh1250000.00250000.000000
2Anita2180000.0083333.333333
7Arjun2100000.0083333.333333
8Pooja255000.0083333.333333
9Rahul252000.0083333.333333
10Sneha258000.0083333.333333
23Ritu255000.0083333.333333
6Kavya3110000.0067333.333333
19Rohit350000.0067333.333333
20Swati342000.0067333.333333
3Vikram4190000.0094000.000000
11Kiran495000.0094000.000000
12Divya470000.0094000.000000
13Amit468000.0094000.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
FirstNameSalaryTotalPayrollCompanyAvgTotalEmployeesPctOfPayroll
Rajesh250000.002192000.0087680.0000002511.41
Vikram190000.002192000.0087680.000000258.67
Anita180000.002192000.0087680.000000258.21
Suresh130000.002192000.0087680.000000255.93
Meena120000.002192000.0087680.000000255.47
Kavya110000.002192000.0087680.000000255.02
Deepak105000.002192000.0087680.000000254.79
Arjun100000.002192000.0087680.000000254.56
Kiran95000.002192000.0087680.000000254.33
Sanjay90000.002192000.0087680.000000254.11
Neha75000.002192000.0087680.000000253.42
Divya70000.002192000.0087680.000000253.19
Geeta70000.002192000.0087680.000000253.19
Amit68000.002192000.0087680.000000253.10
Lakshmi62000.002192000.0087680.000000252.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
DepartmentNameFirstNameSalaryDeptHeadcountDeptMinSalaryDeptMaxSalaryDiffFromDeptAvg
ExecutiveRajesh250000.001250000.00250000.000.000000
FinanceSuresh130000.00360000.00130000.0046000.000000
FinanceLakshmi62000.00360000.00130000.00-22000.000000
FinanceManoj60000.00360000.00130000.00-24000.000000
HRMeena120000.00245000.00120000.0037500.000000
HRPriya45000.00245000.00120000.00-37500.000000
ITVikram190000.00768000.00190000.0096000.000000
ITKiran95000.00768000.00190000.001000.000000
ITSanjay90000.00768000.00190000.00-4000.000000
ITNeha75000.00768000.00190000.00-19000.000000
ITGeeta70000.00768000.00190000.00-24000.000000
ITDivya70000.00768000.00190000.00-24000.000000
ITAmit68000.00768000.00190000.00-26000.000000
MarketingKavya110000.00342000.00110000.0042666.666667
MarketingRohit50000.00342000.00110000.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
CategoryNameProductNameRevenueCategoryRevenuePctOfCategoryPctOfTotal
Accessories27 inch Monitor510300.00711285.0071.744.10
AccessoriesWireless Mouse84660.00711285.0011.900.68
AccessoriesMechanical Keyboard69250.00711285.009.740.56
AccessoriesUSB-C Hub47075.00711285.006.620.38
DesksStanding Desk325600.00325600.00100.002.62
DesktopsLenovo ThinkCentre1438250.002354000.0061.1011.57
DesktopsAcer Aspire Desktop915750.002354000.0038.907.37
LaptopsMacBook Air M33408000.006174750.0055.1927.41
LaptopsHP Pavilion 141938000.006174750.0031.3915.59
LaptopsDell Inspiron 15828750.006174750.0013.426.67
MobilesiPhone 151774500.002467000.0071.9314.27
MobilesSamsung Galaxy A55355000.002467000.0014.392.86
MobilesRedmi Note 13337500.002467000.0013.682.71
Office ChairsErgonomic Office Chair201000.00372700.0053.931.62
Office ChairsBasic Office Chair171700.00372700.0046.071.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
SaleDateRegionEmployeeIDTotalAmountRegionTotalEmployeeTotalRegionEmployeeTotal
2025-01-07East1085000.00995500.00500850.0085000.00
2025-01-14East7165000.00995500.00826000.00295000.00
2025-01-17East8140000.00995500.00546000.00230000.00
2025-01-19East890000.00995500.00546000.00230000.00
2025-01-23East7130000.00995500.00826000.00295000.00
2025-01-27East23120000.00995500.00591480.00360000.00
2025-01-28East23240000.00995500.00591480.00360000.00
2025-01-31East925500.00995500.00144800.0025500.00
2025-01-02North105400.00453780.00500850.00103800.00
2025-01-05North836000.00453780.00546000.0036000.00
2025-01-13North925500.00453780.00144800.0073500.00
2025-01-15North108400.00453780.00500850.00103800.00
2025-01-15North7240000.00453780.00826000.00240000.00
2025-01-24North108500.00453780.00500850.00103800.00
2025-01-25North1059500.00453780.00500850.00103800.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
SaleDateRegionTotalAmountRegionTotalRunningTotal
2025-01-02South240000.001117800.00240000.00
2025-01-06South195000.001117800.00435000.00
2025-01-11South6300.001117800.00441300.00
2025-01-16South30000.001117800.00471300.00
2025-01-22South195000.001117800.00666300.00
2025-01-30South55000.001117800.00721300.00
2025-02-03South165000.001117800.00886300.00
2025-02-05South14000.001117800.00900300.00
2025-02-19South5000.001117800.00905300.00
2025-02-22South17500.001117800.00922800.00
2025-02-28South195000.001117800.001117800.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
FirstNameDepartmentIDSalaryDeptAvg
Anita2180000.0083333.333333
Arjun2100000.0083333.333333
Kavya3110000.0067333.333333
Vikram4190000.0094000.000000
Kiran495000.0094000.000000
Meena5120000.0082500.000000
Suresh6130000.0084000.000000
Deepak7105000.0072500.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
MonthNameMonthRevenuePctOfTotal
January2609130.0016.69
February1678350.0010.74
March3100570.0019.83
April3223180.0020.62
May1898440.0012.14
June3124020.0019.98

Practice Questions

  1. Show each order item with the total value of its order (no GROUP BY).
  2. Show each customer's orders with that customer's total order count.
  3. List employees whose salary is more than 1.2x their department average.
  4. Show each region's share (%) of the total 2025 sales.
  5. For each sale, show the highest single sale amount of that region.