Historical Context & Motivation
Long before SQL gained the ability to express them declaratively, running totals and moving averages were fundamental techniques in statistics, finance, and signal processing. A running total—also called a cumulative sum—tracks the accumulated value of a series as each new observation arrives, while a moving average smooths noisy data by averaging a sliding window of recent values. Before the SQL:2003 standard introduced window functions, database developers had to resort to correlated subqueries, self-joins, or cursor-based procedural code—approaches that were error-prone, hard to read, and often performed poorly on large data sets.
The central question this lesson addresses is straightforward yet powerful: how can we compute an aggregate that grows or slides across an ordered sequence of rows, without collapsing those rows into a single group? Window functions answer this by letting every row retain its identity while simultaneously seeing a computed aggregate over a defined frame of neighboring rows.
Core Principles & Definitions
Before diving into syntax, it is essential to internalize the building blocks that make running totals and moving averages possible within SQL's window function framework. Each concept below maps directly to a clause or keyword you will write in production queries.
OVER Clause
PARTITION BY
ORDER BY (within OVER)
Frame Specification
Running Total vs. Moving Average
Visual Explanation — How the Window Frame Slides
The diagram below illustrates the fundamental difference between a running total and a 3-row moving average applied to the same ordered sequence of daily revenue values. Notice how the running total's frame always starts at the first row and grows, while the moving average's frame maintains a fixed width and slides forward.
As the current row advances from Monday to Friday, the running total's frame expands to include every preceding row plus the current one, so its SUM is monotonically non-decreasing (assuming non-negative values). The moving average's frame, by contrast, always includes exactly the current row and its two predecessors. Once Wednesday is reached, the first row (Monday) begins to fall out of the moving average frame as Thursday becomes current. This fixed-width sliding behavior is what smooths short-term volatility and reveals longer-term trends.
SQL Syntax & Frame Specifications
The SQL expressions for running totals and moving averages follow a consistent template. The key syntactic element is the frame clause within the OVER specification. Understanding the frame clause is critical because the default frame—which many developers overlook—varies between database engines and can silently produce incorrect results.
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Note RANGE, not ROWS. With RANGE, rows sharing the same ORDER BY value (ties) are all included in the frame together, which can produce unexpected running totals when sort keys are not unique. Always specify an explicit ROWS BETWEEN clause to avoid surprises.Frame Boundary Options & Classification
SQL window frames are defined by a start boundary and an end boundary. The combination of these boundaries determines whether the aggregate behaves as a running total, a moving average, a moving sum, or even a look-ahead computation. The following diagram classifies the most common frame patterns and maps each to its analytical use case.
| Pattern | Frame Clause | Common Aggregate | Typical Use Case |
|---|---|---|---|
| Running Total | ROWS UNBOUNDED PRECEDING … CURRENT ROW | SUM | YTD revenue, account balance |
| Moving Average | ROWS N PRECEDING … CURRENT ROW | AVG | 7-day smoothed metrics, stock price |
| Rolling Sum | ROWS N PRECEDING … CURRENT ROW | SUM | 30-day active users, trailing revenue |
| Partition Total | ROWS UNBOUNDED PRECEDING … UNBOUNDED FOLLOWING | SUM | Percent-of-total calculations |
| Centered MA | ROWS N PRECEDING … N FOLLOWING | AVG | Seasonal decomposition |
Worked Example — Daily Revenue Analysis
Suppose we have a table called daily_revenue with columns sale_date, region, and amount. We want to compute, for each region, both a running total and a 3-day moving average of daily revenue.
SELECT sale_date, region, amount FROM daily_revenue ORDER BY region, sale_date;SUM(amount) OVER (
PARTITION BY region
ORDER BY sale_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_totalAVG(amount) OVER (
PARTITION BY region
ORDER BY sale_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS moving_avg_3dSELECT
sale_date,
region,
amount,
SUM(amount) OVER (
PARTITION BY region
ORDER BY sale_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total,
AVG(amount) OVER (
PARTITION BY region
ORDER BY sale_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS moving_avg_3d
FROM daily_revenue
ORDER BY region, sale_date;Strengths, Limitations & Comparisons
Window functions offer a declarative, set-based approach to running computations, but they are not the only option. Understanding when to prefer window functions over alternatives—and recognizing their limitations—helps you make sound engineering decisions.
| Approach | Strengths | Limitations |
|---|---|---|
| Window Functions (SUM/AVG OVER) | Declarative, readable, composable with other window functions. Preserves row-level detail. Optimized by modern query planners. | Cannot be used in WHERE or HAVING (must be wrapped in a CTE/subquery). Memory overhead for very large partitions. Not all engines support all frame types. |
| Correlated Subquery | Works in any SQL dialect, including older engines without window function support. Conceptually explicit. | O(n²) worst-case per partition—executes a subquery for every row. Verbose and error-prone for complex frames. |
| Self-Join | No window function dependency. Clear join semantics for auditing. | Produces row explosion (cartesian-like growth). Hard to parameterize frame width. Poor performance at scale. |
| Application-Level Loop | Full language expressiveness. Can handle arbitrarily complex logic. | Transfers all data to the client. Loses database-level parallelism and optimization. Harder to maintain in data pipelines. |
Connection to Advanced Window Function Techniques
Running totals and moving averages represent the introductory tier of window function mastery. They naturally lead into a family of more advanced analytical patterns that you will encounter in data engineering, machine learning feature stores, and real-time analytics dashboards. The table below maps each introductory concept to its advanced extension.
| This Lesson (Intro) | Advanced Extension | What Changes |
|---|---|---|
| Running total (SUM OVER) | Conditional running total (SUM with CASE/FILTER) | Accumulate only rows meeting a condition, e.g., SUM(CASE WHEN status = 'shipped' THEN amount END). |
| Moving average (AVG OVER) | Exponentially weighted moving average (EWMA) | EWMA gives exponentially decreasing weights to older observations. Not natively supported in SQL but can be approximated with recursive CTEs. |
| ROWS frame | RANGE frame with interval arithmetic | RANGE BETWEEN INTERVAL '7' DAY PRECEDING AND CURRENT ROW handles irregular time series where rows are not daily. |
| Single window expression | Named WINDOW clause (WINDOW w AS …) | Define a reusable window specification once and reference it from multiple functions—reduces duplication and errors. |
| Running total over ordered data | Sessionization & gap-and-island problems | Running totals combined with LAG/LEAD and conditional resets identify session boundaries in clickstream or event data. |
As you progress, pay special attention to how RANGE with temporal intervals solves a problem that ROWS cannot: computing a true calendar-based moving average even when some dates have no rows. In many production data warehouses, dates are densified through a calendar dimension table first, and then the ROWS-based moving average is applied—an architectural pattern worth understanding before you encounter it in a pipeline.
Practice Problems
orders(order_date DATE, total DECIMAL) with values [(2024-01-01, 50), (2024-01-02, 30), (2024-01-03, 70), (2024-01-04, 20)], write a query that returns each row with a cumulative running total. What is the running total on 2024-01-03?orders table, write a query that computes a 3-day moving average of total. What value does the moving average return on 2024-01-02 (the second row), and why?signups(signup_date DATE, plan VARCHAR, count INT). Write a single query that returns, for each plan, the sign-up date, daily count, running total of sign-ups since the plan's first record, and a 7-day moving average of daily sign-ups. Explain the frame choices.Summary
SQL window functions enable running totals and moving averages by computing aggregates across a defined window frame without collapsing rows. A running total uses SUM with an expanding frame (UNBOUNDED PRECEDING to CURRENT ROW), accumulating all preceding values. A moving average uses AVG with a sliding frame of fixed width (N PRECEDING to CURRENT ROW), smoothing recent values.
The OVER clause converts standard aggregates into window functions, PARTITION BY segments data into independent groups, ORDER BY establishes the logical row sequence, and the ROWS BETWEEN clause precisely controls which rows are included. Always specify an explicit frame to avoid the default RANGE behavior, which groups tied ORDER BY values and can produce unexpected results. These patterns form the foundation for advanced techniques including conditional running totals, EWMA approximations, interval-based RANGE frames, and sessionization queries.