Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 3

Window Functions for Running Totals and Moving Averages

Recommended resources

Hello! In our last lesson, you learned how to use window functions like RANK, LAG, and LEAD to perform calculations across entire partitions of your data. We saw how this approach is more powerful than simple GROUP BY aggregations because it preserves individual rows.

In this lesson, we'll take that concept a step further. Instead of operating on the whole partition, you will learn to define a smaller, moving "window" or frame of rows around the current row. This is the key to calculating two of the most common metrics in data analysis: running totals and moving averages. Mastering this will allow you to analyze trends, track cumulative progress, and smooth out noisy data—all essential skills for a data scientist.

1. Calculating Running Totals

A running total (or cumulative sum) calculates the sum of a value from the start of a sequence up to the current row. It's used everywhere, from tracking cumulative sales in a quarter to monitoring the total number of user sign-ups over time.

In the last lesson, our window functions operated on the entire partition. To calculate a running total, we need to tell SQL to create a window that starts at the first row and ends at the current row. This is done using a frame clause within the OVER() clause.

This image illustrates the basic syntax for a running total. The `SUM()` function is applied over a window that is ordered by date, accumulating the sales amount for each row.

The explicit way to define this frame is with ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Let's break that down:

  • ROWS: Specifies that the frame is defined by a physical number of rows.
  • UNBOUNDED PRECEDING: The frame starts at the very first row of the partition.
  • CURRENT ROW: The frame ends at the row currently being processed.

This article provides a clear explanation and examples.

5 Practical Examples of Using ROWS BETWEEN in SQL

This article from learnsql.com gives a great breakdown of the ROWS clause and its options.

First, read the section "ROWS Clause: Syntax and Options" to understand the different ways you can define a frame's boundaries. Then, study Example 1, which shows how ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW is used to calculate a simple running total of revenue. Finally, review Example 4, which demonstrates how this concept is combined with PARTITION BY to calculate separate running totals for different social networks.

As you saw in the article, when ORDER BY is used, SQL often assumes ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW as the default frame for aggregate functions like SUM(). This means that a simpler query often works for running totals:

SELECT
  SaleDate,
  SaleAmount,
  SUM(SaleAmount) OVER (ORDER BY SaleDate) AS RunningTotal
FROM Sales;

While this shortcut is convenient, explicitly writing the full ROWS BETWEEN... clause makes your query clearer and is good practice, especially as you start building more complex frames.


2. Computing Moving Averages

Running totals are great for tracking cumulative growth, but sometimes we need to understand recent trends and smooth out day-to-day volatility. This is where moving averages (also called rolling averages) come in. A moving average calculates the average of a value over a specific number of recent periods, like a 7-day or 30-day moving average.

To do this, we just need to adjust our window frame. Instead of an UNBOUNDED start, we specify a fixed number of rows to look back. For a 3-day moving average, the frame would include the current day and the two preceding days.

The syntax for this is ROWS BETWEEN 2 PRECEDING AND CURRENT ROW.

This video provides an excellent, step-by-step guide to calculating rolling values.

SQL Running Total | Advance SQL | Rolling N months SUM, AVG, MIN, MAX

This video from Ankit Bansal walks through the concept and implementation of rolling calculations in SQL.

First, watch the introduction to understand what rolling values are and how they are calculated conceptually. Next, see the implementation for a 3-month rolling sum. Pay close attention to the ROWS BETWEEN 2 PRECEDING AND CURRENT ROW syntax. Then, watch the segment on different window frames, which introduces the FOLLOWING keyword and frames that are centered around the current row (e.g., 1 PRECEDING AND 1 FOLLOWING). Finally, see how easily this can be adapted to other calculations in the average, min, and max section.

As the video demonstrated, this technique is incredibly versatile. By simply changing the aggregate function (SUM, AVG, MIN, MAX, COUNT) or the frame definition, you can answer a wide range of analytical questions.

For example, to calculate a 7-day moving average of temperature, partitioned by city, you would write:

SELECT
  city,
  date,
  temperature,
  AVG(temperature) OVER (
    PARTITION BY city
    ORDER BY date
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
  ) AS moving_avg_7_day
FROM weather_data;

3. A Quick but Important Detail: ROWS vs. RANGE

So far, we've only used ROWS to define our frame. There is another keyword, RANGE, that can also be used. While they often produce the same result, they behave differently when there are ties in the ORDER BY column.

  • ROWS: Defines the frame based on a fixed number of physical rows before and after the current row. ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING always includes exactly three rows (if available).
  • RANGE: Defines the frame based on a logical range of values relative to the value in the current row's ordering column. RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING on a price column would include all rows where the price is within 1 unit of the current row's price. If multiple rows have the same price, they are all included.

For most running total and moving average tasks based on sequential data like dates, ROWS is what you want. RANGE is more specialized, for example, if you wanted to find the average price of all items within $10 of the current item's price.

This image provides a great visual comparison.

This image illustrates the difference between `ROWS` and `RANGE` frames. For the `Current row` with a price of 20, the `ROWS` frame includes one row before and one after, regardless of their price. The `RANGE` frame, however, includes all rows where the price value is identical to the current row's price, because the default range for `RANGE` includes all rows with the same ordering value as the current row.

Understanding this distinction will help you write more precise queries and avoid unexpected results when dealing with data that contains duplicate values in your ordering column.


4. Practice Exercises

Let's apply what you've learned. Consider the following table, daily_revenue, which tracks revenue for two different online services.

service date revenue
'A' '2023-10-01' 100
'A' '2023-10-02' 120
'A' '2023-10-03' 80
'A' '2023-10-04' 110
'B' '2023-10-01' 50
'B' '2023-10-02' 60
'B' '2023-10-03' 55
'B' '2023-10-04' 70

Problem 1: Calculate the cumulative revenue for each service

Task: For each service, calculate the running total of revenue, ordered by date.

Desired Output:

service date revenue cumulative_revenue
'A' '2023-10-01' 100 100
'A' '2023-10-02' 120 220
'A' '2023-10-03' 80 300
'A' '2023-10-04' 110 410
'B' '2023-10-01' 50 50
'B' '2023-10-02' 60 110
'B' '2023-10-03' 55 165
'B' '2023-10-04' 70 235
Solution
SELECT
  service,
  date,
  revenue,
  SUM(revenue) OVER (
    PARTITION BY service
    ORDER BY date
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS cumulative_revenue
FROM daily_revenue;

Note: The simpler SUM(revenue) OVER (PARTITION BY service ORDER BY date) would also work here, but being explicit with the frame clause is clearer.


Problem 2: Calculate the 3-day moving average revenue

Task: For each service, calculate the 3-day moving average of revenue. For the first two days of each service, the average will be based on the available data (1 and 2 days, respectively).

Desired Output:

service date revenue moving_avg_3day
'A' '2023-10-01' 100 100.00
'A' '2023-10-02' 120 110.00
'A' '2023-10-03' 80 100.00
'A' '2023-10-04' 110 103.33
'B' '2023-10-01' 50 50.00
'B' '2023-10-02' 60 55.00
'B' '2023-10-03' 55 55.00
'B' '2023-10-04' 70 61.67
Solution
SELECT
  service,
  date,
  revenue,
  -- We cast to numeric/decimal to ensure floating-point division
  ROUND(
    AVG(revenue) OVER (
      PARTITION BY service
      ORDER BY date
      ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
    ), 2
  ) AS moving_avg_3day
FROM daily_revenue;

Conclusion

You've now added another crucial tool to your SQL analytics toolkit. By defining explicit window frames, you can move beyond static, whole-partition calculations and perform sophisticated sequential analysis.

Key Takeaways:

  • A window frame defines a specific subset of rows within a partition for a window function to operate on.
  • Running totals are calculated using a frame that grows with each row, typically ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW.
  • Moving averages are calculated using a sliding frame of a fixed size, like ROWS BETWEEN 6 PRECEDING AND CURRENT ROW for a 7-day average.
  • The same frame clause syntax can be used with SUM, AVG, COUNT, MIN, and MAX to compute various rolling metrics.
  • ROWS defines a frame by physical row count, which is what you'll use most often for time-series analysis.

In our next lesson, we'll shift gears and explore how to use CASE statements, both on their own and within aggregate functions. This will enable you to implement conditional logic, create custom categories, and even pivot data from a "long" to a "wide" format—another powerful technique for creating summary reports.