🔥 0
0
Lesson 5 of 6 20 min +200 XP

Window Frames: ROWS and RANGE

Window frames let you define exactly which rows participate in each calculation. Need a 7-day moving average? A running total? Sum of previous 3 rows? Frames give you precise control.

The Frame Clause Syntax

function(...) OVER (
  PARTITION BY ...
  ORDER BY ...
  frame_type BETWEEN frame_start AND frame_end
)

Frame Types

  • ROWS
  • RANGE
  • GROUPS (PostgreSQL 11+)

Frame Boundaries

  • UNBOUNDED PRECEDING
  • n PRECEDING
  • CURRENT ROW
  • n FOLLOWING
  • UNBOUNDED FOLLOWING

Default Frame Behavior

Important: Default Frames
  • Without ORDER BY: RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING (entire partition)
  • With ORDER BY: RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW (running calculation)

ROWS vs RANGE

ROWS: Physical Row Count

ROWS counts actual rows regardless of values.

SELECT
  date,
  revenue,
  SUM(revenue) OVER (
    ORDER BY date
    ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
  ) as sum_3_rows
FROM daily_sales;
date revenue sum_3_rows rows included
Jan 1 100 100 [Jan 1]
Jan 2 150 250 [Jan 1, Jan 2]
Jan 3 120 370 [Jan 1, Jan 2, Jan 3]
Jan 4 200 470 [Jan 2, Jan 3, Jan 4]
Jan 5 180 500 [Jan 3, Jan 4, Jan 5]

RANGE: Value-Based Boundaries

RANGE uses the ORDER BY column's values, not row count.

-- Include all rows with the same date (handles ties)
SELECT
  date,
  sale_id,
  amount,
  SUM(amount) OVER (
    ORDER BY date
    RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) as running_total
FROM sales;
date sale_id amount running_total (RANGE)
Jan 1 S001 100 250
Jan 1 S002 150 250
Jan 2 S003 200 450

Notice: Both Jan 1 rows show 250 because RANGE includes all rows with the same date value.

Moving Averages

7-Day Moving Average

SELECT
  date,
  revenue,
  ROUND(AVG(revenue) OVER (
    ORDER BY date
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
  ), 2) as avg_7_day
FROM daily_sales;
date revenue avg_7_day
Day 1 1000 1000.00
Day 2 1200 1100.00
... ... ...
Day 7 1500 1257.14
Day 8 1100 1285.71

Centered Moving Average

-- 5-day centered average (2 before, current, 2 after)
SELECT
  date,
  value,
  AVG(value) OVER (
    ORDER BY date
    ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING
  ) as centered_avg
FROM timeseries;

Common Frame Patterns

Pattern Frame Clause Use Case
Running total UNBOUNDED PRECEDING AND CURRENT ROW Cumulative sum/count
Entire partition UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING Partition totals, LAST_VALUE
Moving average N PRECEDING AND CURRENT ROW Trailing N-period average
Centered N PRECEDING AND N FOLLOWING Smoothing, outlier detection
Look ahead CURRENT ROW AND N FOLLOWING Forward-looking aggregates

Real-World: Stock Price Analysis

SELECT
  date,
  close_price,
  -- 20-day moving average
  ROUND(AVG(close_price) OVER (
    ORDER BY date
    ROWS BETWEEN 19 PRECEDING AND CURRENT ROW
  ), 2) as ma_20,
  -- 50-day moving average
  ROUND(AVG(close_price) OVER (
    ORDER BY date
    ROWS BETWEEN 49 PRECEDING AND CURRENT ROW
  ), 2) as ma_50,
  -- Highest in last 52 weeks
  MAX(close_price) OVER (
    ORDER BY date
    ROWS BETWEEN 251 PRECEDING AND CURRENT ROW
  ) as high_52w,
  -- Lowest in last 52 weeks
  MIN(close_price) OVER (
    ORDER BY date
    ROWS BETWEEN 251 PRECEDING AND CURRENT ROW
  ) as low_52w
FROM stock_prices;

Real-World: Cumulative Distribution

-- What percentage of orders are below each value?
SELECT
  order_value,
  COUNT(*) OVER (ORDER BY order_value) as orders_at_or_below,
  COUNT(*) OVER () as total_orders,
  ROUND(100.0 * COUNT(*) OVER (ORDER BY order_value) / COUNT(*) OVER (), 2) as percentile
FROM orders;
order_value orders_at_or_below total_orders percentile
$25 150 1000 15.00%
$50 450 1000 45.00%
$100 820 1000 82.00%

EXCLUDE Clause (PostgreSQL 11+)

Fine-tune what's excluded from the frame:

-- Average excluding the current row
AVG(value) OVER (
  ORDER BY date
  ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING
  EXCLUDE CURRENT ROW
)

Options:

  • EXCLUDE CURRENT ROW - Exclude current row
  • EXCLUDE GROUP - Exclude current row and peers
  • EXCLUDE TIES - Exclude peers but keep current row
  • EXCLUDE NO OTHERS - Default, exclude nothing

Key Takeaways

  • ROWS counts physical rows - Exact control, most common for moving averages
  • RANGE uses values - Includes ties, good for date ranges with duplicates
  • Default frame changes with ORDER BY - Add explicit frame for clarity
  • 7-day average = 6 PRECEDING + current - Remember to count correctly!

Next up: Window Functions Assessment - Test your knowledge with practical scenarios.

🧠 Quick Quiz

Test your understanding of this lesson.

1

What does ROWS BETWEEN 2 PRECEDING AND CURRENT ROW include?

2

What's the key difference between ROWS and RANGE?

3

For a 7-day moving average, which frame is correct?