✏️ Explanatory Question
Level: Hard — A deep gotcha most candidates miss; the default frame silently produces wrong running totals with duplicate ORDER BY values.
SUM(amount) OVER (ORDER BY sale_date) and it looks fine — until two sales share the same date. Then the running total jumps, showing the same larger value for both rows instead of incrementing one at a time. Nothing looks wrong in the query. What's happening?
sales (note the duplicate date)| id | sale_date | amount |
|---|---|---|
| 1 | 2026-01-01 | 100 |
| 2 | 2026-01-02 | 200 |
| 3 | 2026-01-02 | 50 |
| 4 | 2026-01-03 | 300 |
The cause: When you specify ORDER BY but no explicit frame, MySQL defaults to RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. With RANGE, "current row" includes all peer rows that share the same ORDER BY value. So both Jan-02 rows are treated as one group — each shows the total of both.
| id | date | amount | RANGE (default) ❌ | ROWS ✓ |
|---|---|---|---|---|
| 1 | 01-01 | 100 | 100 | 100 |
| 2 | 01-02 | 200 | 350 | 300 |
| 3 | 01-02 | 50 | 350 | 350 |
| 4 | 01-03 | 300 | 650 | 650 |
With RANGE, both Jan-02 rows show 350 (the "peers" are lumped together). With ROWS, they correctly increment: 300 then 350.
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW for running totals. Never rely on the default frame — it silently switches to RANGE and produces "peer-grouped" results whenever ORDER BY values tie.
-- THE BUG: no frame -> defaults to RANGE -> ties get lumped
SELECT id, sale_date, amount,
SUM(amount) OVER (ORDER BY sale_date) AS running_total
FROM sales;
-- Both Jan-02 rows show 350
-- THE FIX: explicit ROWS frame -> true row-by-row running total
SELECT id, sale_date, amount,
SUM(amount) OVER (
ORDER BY sale_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total
FROM sales;
-- Correct: 100, 300, 350, 650
-- EVEN BETTER: add a tie-breaker so order is deterministic
SELECT id, sale_date, amount,
SUM(amount) OVER (
ORDER BY sale_date, id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total
FROM sales;
| Frame | Meaning |
|---|---|
ROWS UNBOUNDED PRECEDING | Running total (start → current) |
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW | 7-row moving window |
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING | 3-row centered window |
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING | Reverse running total |