Skip to content
advancedPhase 23 · SQL Advanced Querying

Window Aggregations

Use SUM() OVER, COUNT() OVER, and running totals with window functions.

1h
4 problems
Topic Progress0%

Running Total

Running Total

A running total (cumulative sum) accumulates values as you move through rows. Using SUM() OVER with ORDER BY creates a cumulative sum that grows with each row.

-- Running total of orders per customer
SELECT 
    order_id,
    customer_id,
    order_date,
    total_amount,
    SUM(total_amount) OVER (
        PARTITION BY customer_id 
        ORDER BY order_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS running_total
FROM orders
ORDER BY customer_id, order_date;

The frame clause ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW specifies that the sum includes all rows from the beginning of the partition up to and including the current row.

-- Running total across all orders (no partition)
SELECT 
    order_id,
    order_date,
    total_amount,
    SUM(total_amount) OVER (
        ORDER BY order_date
    ) AS cumulative_revenue
FROM orders;

When ORDER BY is present in the OVER clause but no frame is specified, the default frame is RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. For running totals with potentially duplicate ORDER BY values, use ROWS BETWEEN explicitly to avoid unexpected behavior.

Running totals are essential for financial reports, inventory tracking, and any scenario where you need to see accumulated values over time.

Moving Average

Moving Average

A moving average smooths out data by averaging a fixed number of preceding and/or following rows. It is calculated using AVG() OVER with a frame specification.

-- 7-day moving average of daily sales
SELECT 
    sale_date,
    daily_sales,
    AVG(daily_sales) OVER (
        ORDER BY sale_date
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ) AS moving_avg_7d
FROM daily_sales
ORDER BY sale_date;

The frame ROWS BETWEEN 6 PRECEDING AND CURRENT ROW includes the current row and the 6 rows before it (7 rows total).

-- 3-day moving average (centered: 1 before, current, 1 after)
SELECT 
    sale_date,
    daily_sales,
    AVG(daily_sales) OVER (
        ORDER BY sale_date
        ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
    ) AS moving_avg_centered
FROM daily_sales;
-- Forward-looking moving average
SELECT 
    sale_date,
    daily_sales,
    AVG(daily_sales) OVER (
        ORDER BY sale_date
        ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING
    ) AS forward_avg_3d
FROM daily_sales;

Moving averages are widely used in time-series analysis to identify trends, smooth noise, and forecast future values. Common window sizes are 7 (weekly), 30 (monthly), and 365 (yearly).

Frame Specification

Frame Specification

The frame specification defines exactly which rows are included in the window function's calculation. It controls the boundaries of the window.

-- Frame with ROWS BETWEEN
SELECT 
    order_date,
    total_amount,
    SUM(total_amount) OVER (
        ORDER BY order_date
        ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING
    ) AS five_row_sum
FROM orders;

Frame boundaries:

  • UNBOUNDED PRECEDING: First row of the partition
  • n PRECEDING: n rows before the current row
  • CURRENT ROW: The current row
  • n FOLLOWING: n rows after the current row
  • UNBOUNDED FOLLOWING: Last row of the partition
-- Cumulative sum from start to current row
SELECT 
    order_date,
    total_amount,
    SUM(total_amount) OVER (
        ORDER BY order_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS cumulative
FROM orders;

-- Total of all rows (entire partition)
SELECT 
    order_date,
    total_amount,
    SUM(total_amount) OVER (
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
    ) AS partition_total
FROM orders;

ROWS vs RANGE:

  • ROWS is physical: counts actual rows
  • RANGE is logical: includes all rows with the same ORDER BY value
-- ROWS: exactly 3 rows (may not include all ties)
SUM(amount) OVER (ORDER BY date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)

-- RANGE: includes all rows with same date value
SUM(amount) OVER (ORDER BY date RANGE BETWEEN 2 PRECEDING AND CURRENT ROW)

Always specify the frame explicitly when using ORDER BY in window functions to avoid relying on database-specific defaults.

Common Aggregation Patterns

Common Aggregation Patterns

Window aggregations enable powerful analytical patterns for reporting and data analysis.

Cumulative percentage:

-- Cumulative percentage of total revenue
SELECT 
    order_date,
    daily_revenue,
    SUM(daily_revenue) OVER (ORDER BY order_date) AS cumulative,
    SUM(daily_revenue) OVER (ORDER BY order_date) / 
    SUM(daily_revenue) OVER () * 100 AS cumulative_pct
FROM daily_revenue;

Running count and min/max:

-- Running count of orders and running minimum order amount
SELECT 
    order_id,
    order_date,
    total_amount,
    COUNT(*) OVER (
        ORDER BY order_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS running_count,
    MIN(total_amount) OVER (
        ORDER BY order_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS running_min
FROM orders;

Year-to-date calculations:

-- Year-to-date revenue
SELECT 
    order_date,
    total_amount,
    SUM(total_amount) OVER (
        PARTITION BY YEAR(order_date)
        ORDER BY order_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS ytd_revenue
FROM orders;

Percent of partition total:

-- Each order as a percentage of its customer's total spending
SELECT 
    order_id,
    customer_id,
    total_amount,
    total_amount / SUM(total_amount) OVER (
        PARTITION BY customer_id
    ) * 100 AS pct_of_customer_total
FROM orders;

Cumulative distribution:

-- CUME_DIST: fraction of rows with values less than or equal to current row
SELECT 
    product_name,
    price,
    CUME_DIST() OVER (ORDER BY price) AS cumulative_distribution
FROM products;

These patterns replace complex self-joins and subqueries with elegant, single-query solutions.

Practice Problems

0/4solved
Daily Revenue Running Total

Write a query to calculate a running total of daily revenue. Return sale_date, daily_revenue, and running_total.

Solution
SELECT 
    sale_date,
    daily_revenue,
    SUM(daily_revenue) OVER (
        ORDER BY sale_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS running_total
FROM daily_revenue;
7-Day Moving Average Sales

Write a query to calculate a 7-day moving average of daily sales. Return sale_date, daily_sales, and moving_avg_7d.

Solution
SELECT 
    sale_date,
    daily_sales,
    AVG(daily_sales) OVER (
        ORDER BY sale_date
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ) AS moving_avg_7d
FROM daily_sales;
Customer Running Total

Write a query to calculate a running total of spending per customer. Return order_id, customer_id, order_date, total_amount, and running_total.

Solution
SELECT 
    order_id,
    customer_id,
    order_date,
    total_amount,
    SUM(total_amount) OVER (
        PARTITION BY customer_id
        ORDER BY order_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS running_total
FROM orders;
Cumulative Revenue Percentage

Write a query to calculate each day's revenue as a cumulative percentage of total revenue. Return sale_date, daily_revenue, cumulative_revenue, and cumulative_pct.

Solution
SELECT 
    sale_date,
    daily_revenue,
    SUM(daily_revenue) OVER (
        ORDER BY sale_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS cumulative_revenue,
    SUM(daily_revenue) OVER (
        ORDER BY sale_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) / SUM(daily_revenue) OVER () * 100 AS cumulative_pct
FROM daily_revenue;

Quiz

1. What does ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW define?

Question 1 options

2. What is the difference between ROWS and RANGE in frame specifications?

Question 2 options

3. How do you calculate a 30-day moving average?

Question 3 options

4. What happens if you use SUM() OVER (ORDER BY date) without specifying a frame?

Question 4 options

Flashcards

Question

How do you calculate a running total?

Answer

SUM(col) OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW). The frame starts at the first row and ends at the current row.

Question

How do you calculate a moving average?

Answer

AVG(col) OVER (ORDER BY date ROWS BETWEEN n PRECEDING AND CURRENT ROW). For a 7-day average, use 6 PRECEDING (6 + current = 7 rows).

Question

What frame do you use for a cumulative sum from the start?

Answer

ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. This includes all rows from the first row of the partition to the current row.

Question

When should you use ROWS instead of RANGE?

Answer

Use ROWS when you want physical row counting. Use RANGE when you want to include all rows with the same ORDER BY value. ROWS is more predictable for running totals.

Question

What is SQL Window Aggregations?

Answer

SQL Window Aggregations is a key concept in SQL databases.

Revision Notes

Key Takeaways

  • 1.Running totals use SUM() OVER with ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  • 2.Moving averages use AVG() OVER with a fixed row window (e.g., 6 PRECEDING for 7-day)
  • 3.Always specify the frame explicitly to avoid default RANGE behavior
  • 4.ROWS is physical row counting; RANGE is logical value-based

Interview Tips

  • Explain the difference between ROWS and RANGE frame specifications
  • Show how to calculate a running total with proper frame clause
  • Discuss when to use UNBOUNDED PRECEDING vs a fixed offset
  • Mention that default frame with ORDER BY is RANGE, not ROWS

Cheat Sheet

Window Aggregations Cheat Sheet

Running Total

SUM(col) OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)

Moving Average

AVG(col) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) -- 7-day

Frame Boundaries

  • UNBOUNDED PRECEDING: first row of partition
  • n PRECEDING: n rows before current
  • CURRENT ROW: current row
  • n FOLLOWING: n rows after current
  • UNBOUNDED FOLLOWING: last row of partition

ROWS vs RANGE

  • ROWS: physical row counting
  • RANGE: includes all rows with same ORDER BY value

Default Frame

  • With ORDER BY: RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  • Without ORDER BY: ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING