SQL Running Totals and Moving Averages with Window Functions
A running total adds each row to everything before it; a moving average smooths values over the last few rows. Both are window functions: an aggregate like SUM or AVG with an OVER clause that says which rows to include.
Examples run in MySQL 8 and PostgreSQL · Updated
In short
SUM(x) OVER (ORDER BY d)is a running total.- Add
PARTITION BYto restart the total for each group. ROWS BETWEEN 2 PRECEDING AND CURRENT ROWlimits the window to three rows.- Unlike
GROUP BY, every row stays in the result.
How the window grows
With ORDER BY inside OVER, the window for each row runs from the first row up to the current one, so SUM becomes a running total. Without ORDER BY, every row would see the whole partition and get the grand total.
A frame clause fixes the window size. ROWS BETWEEN 2 PRECEDING AND CURRENT ROW means the current row and the two before it, a 3-row moving window. The first rows simply have fewer rows to average.
Ties in the sort column
The default frame is RANGE, which treats rows with the same ORDER BY value as one step, so tied rows show the same running total. Add a tie-breaker or use ROWS when each row should step on its own.
The example tables
Every example on this page uses these tables. They are small on purpose, so you can check each result by eye.
orders
| order_id | customer_id | order_date | amount |
|---|---|---|---|
| 101 | 1 | 2025-01-05 | 120 |
| 102 | 1 | 2025-01-20 | 80 |
| 103 | 2 | 2025-01-07 | 200 |
| 104 | 1 | 2025-02-02 | 150 |
| 105 | 3 | 2025-02-10 | 90 |
| 106 | 2 | 2025-02-25 | 60 |
| 107 | 3 | 2025-03-01 | 130 |
| 108 | 1 | 2025-03-03 | 40 |
Examples
Running total of revenue
Each row adds its amount to the total so far.
SELECT order_id, order_date, amount,
SUM(amount) OVER (ORDER BY order_date) AS running_total
FROM orders
ORDER BY order_date;| order_id | order_date | amount | running_total |
|---|---|---|---|
| 101 | 2025-01-05 | 120 | 120 |
| 103 | 2025-01-07 | 200 | 320 |
| 102 | 2025-01-20 | 80 | 400 |
| 104 | 2025-02-02 | 150 | 550 |
| 105 | 2025-02-10 | 90 | 640 |
| 106 | 2025-02-25 | 60 | 700 |
| 107 | 2025-03-01 | 130 | 830 |
| 108 | 2025-03-03 | 40 | 870 |
Running total per customer
PARTITION BY restarts the total for each customer.
SELECT customer_id, order_date, amount,
SUM(amount) OVER (PARTITION BY customer_id ORDER BY order_date) AS customer_total
FROM orders
ORDER BY customer_id, order_date;| customer_id | order_date | amount | customer_total |
|---|---|---|---|
| 1 | 2025-01-05 | 120 | 120 |
| 1 | 2025-01-20 | 80 | 200 |
| 1 | 2025-02-02 | 150 | 350 |
| 1 | 2025-03-03 | 40 | 390 |
| 2 | 2025-01-07 | 200 | 200 |
| 2 | 2025-02-25 | 60 | 260 |
| 3 | 2025-02-10 | 90 | 90 |
| 3 | 2025-03-01 | 130 | 220 |
3-order moving average
The average of the current order and the two before it.
SELECT order_date, amount,
ROUND(AVG(amount) OVER (ORDER BY order_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW), 1) AS moving_avg_3
FROM orders
ORDER BY order_date;| order_date | amount | moving_avg_3 |
|---|---|---|
| 2025-01-05 | 120 | 120.0 |
| 2025-01-07 | 200 | 160.0 |
| 2025-01-20 | 80 | 133.3 |
| 2025-02-02 | 150 | 143.3 |
| 2025-02-10 | 90 | 106.7 |
| 2025-02-25 | 60 | 100.0 |
| 2025-03-01 | 130 | 93.3 |
| 2025-03-03 | 40 | 76.7 |
Common mistakes
- Missing ORDER BY in OVER.
SUM(amount) OVER ()gives the grand total on every row, not a running total. - Ties with the default frame. Rows sharing a date get the same running total under
RANGE. Add a unique tie-breaker such as an id. - A self join instead. Summing earlier rows with a self join works but is slow and easy to get wrong. Window functions are the expected answer.
Practise Running Totals & Moving Averages
Interview questions that use this concept, easiest first. Each one has the step-by-step visualizer and answer checking.
3-Day Moving Average of Orders
Smooth daily order counts with a trailing three-day average.
Accounts That Went Negative
Use running balances to find accounts that ever overdrew and when it first happened.
Products Driving 80% of Revenue
Find the smallest set of top products whose cumulative revenue reaches 80% of the total.
Last Passenger to Board
Find the last person who can board a bus with a 1000 kg weight limit.
User Session Reconstruction
Group raw click events into sessions using a 30-minute inactivity rule.
Three-Month Salary Window
Each employee's rolling three-month salary total, excluding their most recent month.
Want a full set? Work through the Window Functions study plan.
Questions people ask
- What is the difference between ROWS and RANGE?
- ROWS counts physical rows; RANGE groups rows with equal ORDER BY values. For a fixed-size moving window, use ROWS.
- Can I compute a 7-day rolling sum?
- If there is exactly one row per day, ROWS BETWEEN 6 PRECEDING AND CURRENT ROW works. With gaps between days, fill in missing dates first, or join on a date range.