Skip to content
Laviyx Learn

Try

SQL course menu

SQL Window Functions Explained: OVER, PARTITION BY and Running Totals

A beginner-friendly introduction to SQL window functions: how OVER() works, aggregates without collapsing rows, PARTITION BY, ORDER BY for running totals, window frames, and the ranking and offset functions.

Advanced9 min readUpdated
On this page

Window functions are one of the most powerful features in SQL, and one of the most asked-about in interviews. They let you calculate across a set of rows (a total, an average, a rank, the previous value) while keeping every row in the result.

The problem they solve#

With GROUP BY, you get one row per group. You can see each department's average salary, but you lose the individual employees:

SQLRun
SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department;
Output
department  | avg_salary
------------+-----------
Engineering | 79500
Marketing   | 46000
Sales       | 56500

What if you want each employee next to their department's average? That is exactly what a window function does.

The OVER clause#

Add OVER (...) after an aggregate, and it stops collapsing rows. The empty OVER () means "the window is every row":

SQLRun
SELECT name, department, salary,
       SUM(salary) OVER () AS total_payroll
FROM employees;
Output
name  | department  | salary | total_payroll
------+-------------+--------+--------------
Asha  | Sales       | 52000  | 318000
Ravi  | Engineering | 78000  | 318000
Meera | Engineering | 81000  | 318000
John  | Marketing   | 46000  | 318000
Priya | Sales       | 61000  | 318000

Every row is still here, and each has the total next to it. That makes per-row calculations easy, such as each person's share of the payroll:

SQLRun
SELECT name, salary,
       ROUND(100.0 * salary / SUM(salary) OVER (), 1) AS pct_of_payroll
FROM employees
ORDER BY salary DESC;
Output
name  | salary | pct_of_payroll
------+--------+---------------
Meera | 81000  | 25.5
Ravi  | 78000  | 24.5
Priya | 61000  | 19.2
Asha  | 52000  | 16.4
John  | 46000  | 14.5

PARTITION BY: a window per group#

PARTITION BY splits the rows into groups, and the function is calculated separately within each one, like GROUP BY without collapsing:

SQLRun
SELECT name, department, salary,
       AVG(salary) OVER (PARTITION BY department) AS dept_avg
FROM employees
ORDER BY department, salary DESC;
Output
name  | department  | salary | dept_avg
------+-------------+--------+---------
Meera | Engineering | 81000  | 79500
Ravi  | Engineering | 78000  | 79500
John  | Marketing   | 46000  | 46000
Priya | Sales       | 61000  | 56500
Asha  | Sales       | 52000  | 56500

More in PARTITION BY.

ORDER BY inside OVER: running totals#

Adding ORDER BY inside OVER makes the window grow row by row: each row sees itself and every row before it. With SUM, that is a running total:

SQLRun
SELECT month, region, revenue,
       SUM(revenue) OVER (PARTITION BY region ORDER BY month) AS running_total
FROM monthly_sales
ORDER BY region, month;
Output
month   | region | revenue | running_total
--------+--------+---------+--------------
2026-01 | North  | 120     | 120
2026-02 | North  | 150     | 270
2026-03 | North  | 110     | 380
2026-04 | North  | 170     | 550
2026-01 | South  | 90      | 90
2026-02 | South  | 90      | 180
2026-03 | South  | 130     | 310
2026-04 | South  | 125     | 435

The running total restarts for each region because of PARTITION BY region.

Window frames: rolling calculations#

You can control exactly which rows the window contains with a frame. A rolling three-month total:

SQLRun
SELECT region, month, revenue,
       SUM(revenue) OVER (
         PARTITION BY region
         ORDER BY month
         ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
       ) AS rolling_3
FROM monthly_sales
ORDER BY region, month;
Output
region | month   | revenue | rolling_3
-------+---------+---------+----------
North  | 2026-01 | 120     | 120
North  | 2026-02 | 150     | 270
North  | 2026-03 | 110     | 380
North  | 2026-04 | 170     | 430
South  | 2026-01 | 90      | 90
South  | 2026-02 | 90      | 180
South  | 2026-03 | 130     | 310
South  | 2026-04 | 125     | 345

In April, North's window holds February, March and April: 150 + 110 + 170 = 430. Rolling averages, used to smooth noisy data, work the same way with AVG.

The full syntax#

SQL
function(...) OVER (
  PARTITION BY columns      -- optional: separate windows per group
  ORDER BY columns          -- optional: order within each window
  ROWS BETWEEN ... AND ...  -- optional: which rows the window includes
)

Types of window functions#

TypeFunctionsLesson
AggregateSUM, AVG, COUNT, MIN, MAXthis one
RankingROW_NUMBER, RANK, DENSE_RANK, NTILEROW_NUMBER, RANK, DENSE_RANK
OffsetLAG, LEAD, FIRST_VALUE, LAST_VALUELAG, LEAD

Where window functions can be used#

Window functions are calculated after WHERE, GROUP BY and HAVING, just before ORDER BY. So you can use them in SELECT and ORDER BY, but not in WHERE. To filter on a window result (such as "rank = 1"), wrap the query in a subquery or CTE, as you will see in the next lessons.

Window functions are supported by PostgreSQL, SQL Server, Oracle, SQLite 3.25+, and MySQL 8.0+.

Common mistakes#

Practice#

Practice exercise

For each employee, show their name, department, salary, and how much more or less they earn than their department's average, in a column called vs_dept_avg.

Show hint

salary - AVG(salary) OVER (PARTITION BY department).

Show solution
SQLRun
SELECT name, department, salary,
       salary - AVG(salary) OVER (PARTITION BY department) AS vs_dept_avg
FROM employees
ORDER BY department, salary DESC;
Output
name  | department  | salary | vs_dept_avg
------+-------------+--------+------------
Meera | Engineering | 81000  | 1500
Ravi  | Engineering | 78000  | -1500
John  | Marketing   | 46000  | 0
Priya | Sales       | 61000  | 4500
Asha  | Sales       | 52000  | -4500

With GROUP BY you would need a subquery and a join to get the same result.

Key takeaway#

Finished this lesson?