Hook: Interviewers love this one because it checks whether you can make SQL look backward across rows instead of only answering one row at a time.
Question: What is a rolling average in SQL, and how do you calculate it with window functions?
Answer: A rolling average is the average of the current row plus a fixed set of nearby rows, usually the previous N rows. In SQL, you normally calculate it with AVG(...) OVER (...) and an explicit window frame such as ROWS BETWEEN 2 PRECEDING AND CURRENT ROW. The important idea is that a window function keeps every row in the result, instead of collapsing rows like GROUP BY.
Interview-Ready Answer: I’d say a rolling average is a moving average computed over a sliding window of rows. I’d write it with AVG(value) OVER (ORDER BY time_col ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) for a 3-row window. That gives each row its own average while preserving row-level detail. One detail I’d call out is that ROWS counts physical rows, while RANGE behaves differently around duplicate sort values, so I choose the frame explicitly.
Detailed Explanation: A rolling average is just a sliding lens over ordered data. Imagine a 3-row window moving across a list: for each row, you average that row and the two rows before it. This is the kind of question interviewers use to see whether you understand window functions, which are functions that calculate across a set of rows while still returning one output row per input row.
ORDER BY clause.frame, which means the exact subset of rows that will be used for that calculation.ROWS BETWEEN 2 PRECEDING AND CURRENT ROW.AVG to the values inside that frame and writes the result next to the original row.PARTITION BY, the process restarts inside each group, such as each customer, store, or product.The most common mistake is forgetting the frame entirely. In many SQL engines, AVG(x) OVER (ORDER BY t) is cumulative, not rolling, because the default frame grows from the first row up to the current row. That means early values keep influencing later rows forever. For a true rolling average, you must define a fixed-width frame.
| Choice | What it means | Best use |
|---|---|---|
ROWS | Counts physical rows | Last N records |
RANGE | Groups equal sort values | Value-based ranges |
Use ROWS when you want a simple trailing count, like the last 7 sales rows. Use RANGE only when your database and use case truly need value-based grouping, because rows with the same sort key can all enter the frame together. That is why duplicate timestamps can surprise people.
Rolling averages are great for smoothing noisy data: daily sales, request latency, conversion rate, temperature, or fraud score trends. They help you see the signal without being fooled by one spike. If the business question is, What is the recent trend?
, a rolling average is usually better than a raw daily number.
From a practical point of view, the expensive part is usually the sort. The engine sorts each partition by the ordering column, then scans through it once to produce the window values. A good mental model is O(n log n) for the sort and about O(n) for the window pass, though real engines may use extra memory and can spill to disk on large partitions. If you have a matching index on (partition_key, order_key), the engine may read data in the right order and reduce sorting work.
AVG ignores NULL values. If missing data should count as zero, use COALESCE before averaging, but only if zero is the right business meaning.ROWS counts records, not calendar days, so a missing day does not create an empty slot.Real-World Story: A checkout platform wants a 15-minute rolling average of payment success rate on its dashboard. The goal is to detect a gateway problem quickly, not to average the whole day. An engineer accidentally writes a cumulative average instead of a rolling one, so the dashboard stays near 99% even while the last 15 minutes are failing.
What goes wrong: alerts fire late, support tickets pile up, and logs show a burst of 502 and timeout errors from the payment provider. Users report that cards are failing, but the dashboard looks calm because old healthy traffic is diluting the recent outage. Once the query is fixed to use a real rolling frame, the drop appears within minutes and the on-call engineer sees the problem immediately.
That is the real value of rolling averages: they make recent behavior visible, which is exactly what operations teams and product teams need when the system changes fast.
-- Rolling average example
-- This sample is intentionally tiny so you can see exactly how the window changes.
-- day_num is just the ordering key; in a real table this could be a timestamp or an ID.
WITH sales(day_num, revenue) AS (
VALUES
(1, 100),
(2, 120),
(3, 90),
(4, 150),
(5, NULL),
(6, 180)
)
SELECT
day_num,
revenue,
-- The classic rolling average: current row + two previous rows.
-- AVG ignores NULL, so day 5 still averages only the non-NULL values in the frame.
ROUND(
AVG(revenue) OVER (
ORDER BY day_num
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
),
2
) AS rolling_avg_3_rows,
-- If missing revenue should mean zero, you must say so explicitly.
-- This changes the business meaning, so only do it when zero is correct.
ROUND(
AVG(COALESCE(revenue, 0)) OVER (
ORDER BY day_num
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
),
2
) AS rolling_avg_nulls_as_zero,
-- Strict version: hide the result until the frame contains 3 actual rows.
-- This is useful when the first few partial windows would confuse dashboards.
CASE
WHEN COUNT(*) OVER (
ORDER BY day_num
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) = 3
THEN ROUND(
AVG(revenue) OVER (
ORDER BY day_num
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
),
2
)
END AS strict_3_row_avg
FROM sales
ORDER BY day_num;Follow-up & Tricky Questions:
PARTITION BY customer_id so each customer gets their own independent rolling window.COUNT(*) OVER (...) to hide the first few partial frames until the window is full.AVG treat NULL as zero? No. AVG ignores NULL, so you need COALESCE if zero is the intended value.AVG(x) OVER (ORDER BY t) a rolling average? Usually no. In many engines it is cumulative unless you explicitly set the frame.RANGE? Rows with the same ordered value can all be included together, which makes the window wider than many candidates expect.Common Mistakes:
ORDER BY. Without a stable order, a rolling average has no meaningful sequence. Fix: always order by the time or sequence column.GROUP BY instead of a window. GROUP BY collapses rows, but rolling averages need one output per input row. Fix: use AVG(...) OVER (...).ROWS BETWEEN N PRECEDING AND CURRENT ROW explicitly.AVG, and duplicate sort values can change the frame shape. Fix: decide whether missing data is zero and add a tie-breaker column.Memory Hook: Think of a rolling average like a treadmill window: the belt keeps moving forward, but you only stand on the last few steps.
Cheat Sheet:
AVG(...) OVER (...) keeps row detail.ROWS means last N rows.RANGE is value-based and can include peers.AVG ignores NULL, so missing data needs a business rule.PARTITION BY when each group needs its own window.Practice Tasks:
PARTITION BY for store_id so each store gets its own trend line.