Skip to main content

Window Functions: Computing Across Rows Without Collapsing Them

A window function performs a calculation across a set of rows related to the current row - and unlike GROUP BY, it keeps every individual row in the result.

The "window" is the set of rows the function looks at for each calculation. You define the window with the OVER clause.

Prior lessons

Window functions build on Aggregate Functions, GROUP BY, and ORDER BY. The sample dataset is defined in the Introduction to SQL lesson.

Syntax

FUNCTION() OVER (
PARTITION BY column -- optional: divide rows into groups
ORDER BY column -- optional: define row order within the window
)
  • PARTITION BY - splits rows into independent windows (like GROUP BY, but without collapsing)
  • ORDER BY - orders rows within each window (required for running totals and ranking functions)
  • Both are optional - OVER () with nothing applies the function across all rows

GROUP BY vs window functions

GROUP BYWindow function
Collapses rows?Yes - one row per groupNo - all rows remain
Can mix detail + aggregate?NoYes

Examples

Example 1: Per-customer average alongside each order

With GROUP BY you'd lose the individual order rows. With a window function you keep them and add the average as a new column.

SELECT
order_id,
customer_id,
total_amount,
AVG(total_amount) OVER (PARTITION BY customer_id) AS customer_avg
FROM orders
ORDER BY customer_id, order_id;

Result:

order_idcustomer_idtotal_amountcustomer_avg
11999.00469.33
2159.98469.33
61349.00469.33
32349.00674.00
72999.00674.00
43796.00897.50
93999.00897.50
5449.9037.43
10424.9537.43
8589.9789.97

Each row retains its detail; customer_avg is computed per partition.

Example 2: Overall average without PARTITION BY

Omitting PARTITION BY applies the function across all rows as a single window.

SELECT
order_id,
total_amount,
AVG(total_amount) OVER () AS overall_avg,
total_amount - AVG(total_amount) OVER () AS diff_from_avg
FROM orders
ORDER BY order_id;

Result (first few rows):

order_idtotal_amountoverall_avgdiff_from_avg
1999.00471.58527.42
259.98471.58-411.60
3349.00471.58-122.58
............

Example 3: Running total with ORDER BY in OVER

Adding ORDER BY inside OVER makes the window cumulative: each row's window includes all preceding rows (by the specified order) plus itself.

SELECT
order_id,
order_date,
total_amount,
SUM(total_amount) OVER (ORDER BY order_date) AS running_total
FROM orders
ORDER BY order_date;

Result:

order_idorder_datetotal_amountrunning_total
12024-01-05999.00999.00
22024-01-1059.981058.98
32024-01-15349.001407.98
42024-02-01796.002203.98
52024-02-1049.902253.88
............

Common Mistake

Window functions cannot be used in WHERE or GROUP BY

The window function result is computed at the SELECT step. You cannot filter on it directly in WHERE. Wrap the query in a CTE or subquery.

-- Fails:
SELECT order_id, RANK() OVER (ORDER BY total_amount DESC) AS rnk
FROM orders
WHERE rnk = 1;

-- Works: wrap in a CTE
WITH ranked AS (
SELECT order_id, RANK() OVER (ORDER BY total_amount DESC) AS rnk
FROM orders
)
SELECT * FROM ranked WHERE rnk = 1;

Practice

Write a query that shows each order's order_id, customer_id, total_amount, and the customer's total spend (customer_total) computed as a window function partitioned by customer_id.

Show answer
SELECT
order_id,
customer_id,
total_amount,
SUM(total_amount) OVER (PARTITION BY customer_id) AS customer_total
FROM orders
ORDER BY customer_id, order_id;
Donate to this project