Aggregate Window Functions
You know SUM, AVG, COUNT with GROUP BY. Now use them with OVER() to create running totals, moving averages, and percentage calculations - all while keeping your rows intact.
Running Total
The most common use case: cumulative sum over time.
SELECT
date,
revenue,
SUM(revenue) OVER (ORDER BY date) as running_total
FROM daily_sales;
| date | revenue | running_total |
|---|---|---|
| 2024-01-01 | 1000 | 1000 |
| 2024-01-02 | 1500 | 2500 |
| 2024-01-03 | 1200 | 3700 |
| 2024-01-04 | 1800 | 5500 |
| 2024-01-05 | 2000 | 7500 |
Running Total per Category
Add PARTITION BY to restart the running total for each group:
SELECT
category,
date,
revenue,
SUM(revenue) OVER (
PARTITION BY category
ORDER BY date
) as category_running_total
FROM sales;
| category | date | revenue | running_total |
|---|---|---|---|
| Electronics | Jan 1 | 500 | 500 |
| Electronics | Jan 2 | 700 | 1200 |
| Electronics | Jan 3 | 600 | 1800 |
| Clothing | Jan 1 | 300 | 300 |
| Clothing | Jan 2 | 400 | 700 |
Notice: Running total resets for each category!
Percentage of Total
Calculate what percentage each row contributes:
SELECT
department,
employee,
salary,
ROUND(
100.0 * salary / SUM(salary) OVER (), 2
) as pct_of_company,
ROUND(
100.0 * salary / SUM(salary) OVER (PARTITION BY department), 2
) as pct_of_dept
FROM employees;
| department | employee | salary | pct_of_company | pct_of_dept |
|---|---|---|---|---|
| Engineering | Alice | 120000 | 24.00% | 40.00% |
| Engineering | Bob | 100000 | 20.00% | 33.33% |
| Engineering | Carol | 80000 | 16.00% | 26.67% |
| Sales | Dave | 90000 | 18.00% | 45.00% |
| Sales | Eve | 110000 | 22.00% | 55.00% |
Running Count
Track how many events have occurred:
SELECT
date,
new_customers,
SUM(new_customers) OVER (ORDER BY date) as total_customers,
COUNT(*) OVER (ORDER BY date) as day_number
FROM signups;
| date | new_customers | total_customers | day_number |
|---|---|---|---|
| 2024-01-01 | 15 | 15 | 1 |
| 2024-01-02 | 22 | 37 | 2 |
| 2024-01-03 | 18 | 55 | 3 |
Running Average
Calculate the average of all values up to the current row:
SELECT
date,
temperature,
ROUND(AVG(temperature) OVER (ORDER BY date), 1) as avg_temp_so_far
FROM weather_data;
| date | temperature | avg_temp_so_far |
|---|---|---|
| Mon | 72 | 72.0 |
| Tue | 75 | 73.5 |
| Wed | 68 | 71.7 |
| Thu | 70 | 71.3 |
The ORDER BY Effect
SUM(...) OVER ()
Total of ALL rows.
Same value for every row.
→ 1000 (every row)
SUM(...) OVER (ORDER BY date)
Running total up to current row.
Grows as you go down.
→ 100, 350, 600, 1000
Adding ORDER BY to a window function changes its behavior from "entire partition" to "running/cumulative" calculation. This is because ORDER BY implies a default frame of ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW.
Combining Multiple Window Functions
SELECT
date,
product,
revenue,
-- Running total
SUM(revenue) OVER (ORDER BY date) as running_total,
-- Total (constant)
SUM(revenue) OVER () as grand_total,
-- Percentage of total
ROUND(100.0 * revenue / SUM(revenue) OVER (), 2) as pct_of_total,
-- Rank
RANK() OVER (ORDER BY revenue DESC) as revenue_rank
FROM sales;
Real-World: YTD (Year-to-Date) Calculation
SELECT
date,
EXTRACT(YEAR FROM date) as year,
revenue,
SUM(revenue) OVER (
PARTITION BY EXTRACT(YEAR FROM date)
ORDER BY date
) as ytd_revenue
FROM daily_sales;
| date | year | revenue | ytd_revenue |
|---|---|---|---|
| 2023-12-30 | 2023 | 1200 | 985000 |
| 2023-12-31 | 2023 | 1500 | 986500 |
| 2024-01-01 | 2024 | 2000 | 2000 |
| 2024-01-02 | 2024 | 1800 | 3800 |
YTD resets at the start of each year!
Key Takeaways
- OVER() - Calculates across all rows (same result each row)
- OVER(ORDER BY) - Running/cumulative calculation
- OVER(PARTITION BY) - Restarts calculation per group
- Percentage = value / SUM() OVER() - Common analytics pattern
Next up: Value Functions - LAG, LEAD, FIRST_VALUE, and LAST_VALUE for comparing rows.