Window Functions

Aggregates with GROUP BY collapse rows into one per group. Window functions calculate across a set of related rows without collapsing them — each row keeps its identity and gains a value computed from its "window": a department average beside each salary, a running total, the previous order's date.

The OVER clause

SQL
SELECT first_name, department_id, salary,
       round(AVG(salary) OVER ()) AS avg_of_result
FROM employees
WHERE department_id IN (1, 2);
Result 7 rows
first_namedepartment_idsalaryavg_of_result
Rafael18200072429
Mei29800072429
Jonas28500072429
Amara26800072429
Lucas15200072429
Sofia15400072429
Tomás26800072429

Adding OVER (…) turns an aggregate into a window function. With an empty OVER (), the window is the whole result, so every row shows the same average — but all the rows are still there. Because window functions run after WHERE, it's the average of the Sales and Engineering rows shown, not of the whole company.

PARTITION BY: windows per group

SQL
SELECT first_name, department_id, salary,
       round(AVG(salary) OVER (PARTITION BY department_id)) AS dept_avg,
       round(salary - AVG(salary) OVER (PARTITION BY department_id)) AS vs_dept
FROM employees
WHERE department_id IN (1, 2)
ORDER BY department_id, salary DESC;
Result 7 rows
first_namedepartment_idsalarydept_avgvs_dept
Rafael1820006266719333
Sofia15400062667-8667
Lucas15200062667-10667
Mei2980007975018250
Jonas285000797505250
Amara26800079750-11750
Tomás26800079750-11750

PARTITION BY splits rows into groups, like GROUP BY, but the function's result is attached to every row of the group. Comparing each value with its group's average is now a single, readable query.

Share of a total

SQL
SELECT category, name, round(price * stock, 2) AS stock_value,
       round(100.0 * price * stock / SUM(price * stock) OVER (PARTITION BY category), 1) AS pct_of_category
FROM products
WHERE category IN ('Home', 'Toys')
ORDER BY category, pct_of_category DESC;
Result 5 rows
categorynamestock_valuepct_of_category
HomeCeramic Mug250055.8
HomeLinen Throw147032.8
HomeDesk Lamp51011.4
ToysJigsaw Puzzle 100090065.3
ToysWooden Train Set478.834.7

ORDER BY inside OVER: running totals

SQL
SELECT id, order_date,
       COUNT(*) OVER (ORDER BY order_date) AS orders_so_far
FROM orders
WHERE order_date < '2025-12-31';
Result 8 rows
idorder_dateorders_so_far
12025-09-031
22025-09-142
32025-10-013
42025-10-204
52025-11-025
62025-11-156
72025-12-017
82025-12-128

Adding ORDER BY inside OVER makes the window grow row by row: each row sees itself and every row before it. That turns COUNT and SUM into running totals.

Where window functions can go

Window functions are calculated after WHERE, GROUP BY and HAVING, so they can appear only in the select list and ORDER BY. To filter on one, wrap the query in a CTE or subquery:

SQL
WITH ranked AS (
  SELECT first_name, department_id, salary,
         AVG(salary) OVER (PARTITION BY department_id) AS dept_avg
  FROM employees
)
SELECT first_name, department_id, salary, round(dept_avg) AS dept_avg
FROM ranked
WHERE salary > dept_avg;
Result 5 rows
first_namedepartment_idsalarydept_avg
Rafael18200062667
Mei29800079750
Jonas28500079750
Daniel37100059500
Ethan46000051000

Windows over grouped results

SQL
SELECT strftime('%Y-%m', o.order_date) AS month,
       round(SUM(oi.quantity * oi.unit_price), 2) AS revenue,
       round(SUM(SUM(oi.quantity * oi.unit_price)) OVER (ORDER BY strftime('%Y-%m', o.order_date)), 2) AS cumulative
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.id
WHERE o.status <> 'cancelled'
GROUP BY month
ORDER BY month;
Result 7 rows
monthrevenuecumulative
2025-09109.89109.89
2025-10171.48281.37
2025-1181362.37
2025-12148.89511.26
2026-01203.78715.04
2026-02162877.04
2026-03177.981055.02

The SUM(SUM(…)) OVER (…) looks strange but is logical: the inner SUM is the monthly aggregate, and the outer window SUM accumulates those monthly totals.

Named windows

SQL
SELECT first_name, salary,
       MIN(salary) OVER w AS dept_min,
       MAX(salary) OVER w AS dept_max
FROM employees
WHERE department_id = 1
WINDOW w AS (PARTITION BY department_id);
Result 3 rows
first_namesalarydept_mindept_max
Rafael820005200082000
Lucas520005200082000
Sofia540005200082000

The WINDOW clause defines a window once for several functions. It's supported by PostgreSQL, MySQL 8, SQLite and SQL Server 2022.

Try it yourself

Show each product with its category's cheapest and most expensive price.

Try it yourself
Result