What is the default window frame when OVER includes ORDER BY, and when it omits it?
answer
- adding ORDER BY changes more than the sort
- there is always a frame, even unwritten
- the default is value-based, not row-based
- tied rows are pulled in together
- RANGE UNBOUNDED PRECEDING to CURRENT ROW
basics
~20 sWith ORDER BY and no frame clause, the default is RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, which also pulls in every row tied with the current one. With no ORDER BY, the frame is the whole partition.
solid answer
~40 sAn `OVER` clause with `ORDER BY` but no explicit frame gets the standard default `RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`. The trap is that `CURRENT ROW` in RANGE mode means *the last peer of the current row* — the last row whose ORDER BY value equals this one's. So on tied ordering values a "running total" jumps straight to the group's end total and repeats it for every tied row, instead of accumulating row by row. If `OVER` has no `ORDER BY` at all, there is no meaningful notion of "so far", so the frame is the entire partition and every row sees the same total. The fix when you want strict row-by-row accumulation is to spell the frame out: `ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`.
go deeper
Recall that OVER (PARTITION BY x) with no ORDER BY gives every row in the partition the same aggregate, and that adding ORDER BY turns it into a running value.
Be ready to state the default frame verbatim, explain that RANGE CURRENT ROW means the last tied peer, and show the ROWS rewrite that restores row-by-row accumulation.
Show the judgment: know when peer-inclusive totals are the correct report and when they are a bug, and make frames explicit in shared queries so the next reader is not relying on a default they never learned.
Own the convention: decide whether team standards require an explicit frame on every frame-sensitive window call, and weigh that verbosity against silent result changes when an ordering key stops being unique.
## The three parts of OVER A window specification has up to three parts: `PARTITION BY` splits rows into independent groups, `ORDER BY` orders the rows inside each partition, and the **frame clause** selects which slice of that ordered partition feeds the function for the row currently being computed. The frame is recomputed for every row. When you omit the frame clause, you do not get "no frame" — you get a default one, and which default you get depends on whether the window has an `ORDER BY`. ## Default with ORDER BY If the window has an `ORDER BY` and no frame clause, the default is: ```sql RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ``` Read it carefully, because the surprise is hidden inside `CURRENT ROW`. In **RANGE** mode the bounds are expressed in terms of the *ordering value*, not the row position. Rows that share the same `ORDER BY` value are called **peers**. `CURRENT ROW` as a RANGE end bound means "through the last peer of the current row". So the default frame is: everything from the start of the partition up to and including *all rows tied with me*. That produces the classic tied-value surprise: ```sql -- scores: 50, 70, 70, 90 (one row each) SELECT score, SUM(score) OVER (ORDER BY score) AS default_frame FROM exam_scores; -- 50 -> 50 -- 70 -> 190 (50 + 70 + 70: both peers included) -- 70 -> 190 (the same value repeated) -- 90 -> 280 ``` A reader expecting 50, 120, 190, 280 sees the second value "skipped". Nothing is broken; the default frame simply refuses to split a peer group. ## Default without ORDER BY With no `ORDER BY` in the window, there is no ordering, so "rows preceding me" is undefined. The frame is then the **entire partition** — equivalent to `RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING`. This is exactly why `SUM(amount) OVER (PARTITION BY dept_id)` gives every row of a department the same department total: that is the whole-partition frame at work, and it is the standard way to compute a percent-of-total denominator alongside detail rows. So adding `ORDER BY` to an `OVER` clause does not merely sort — it silently changes the frame from "whole partition" to "start of partition through my peers", which changes the number the function returns. Candidates who think `ORDER BY` inside `OVER` is cosmetic get this wrong. ## Getting the row-by-row behaviour you meant Write the frame explicitly: ```sql SUM(score) OVER (ORDER BY score ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) -- 50, 120, 190, 280 ``` In **ROWS** mode `CURRENT ROW` means this physical row and nothing else, so each tied row gets its own accumulating value. The abbreviated single-bound form `ROWS UNBOUNDED PRECEDING` means the same thing — when only a start bound is given, the end bound is `CURRENT ROW`. A useful habit: if the ordering key is unique (a primary key, a strictly increasing timestamp), the default frame and `ROWS ... CURRENT ROW` agree, so the default is harmless. The moment the ordering key can repeat — a date column on a table with several rows per day, a score, a category — decide deliberately which behaviour you want and spell it out. ## Which behaviour is actually right? Neither default is a bug; both answer a real question. - **Peer-inclusive (RANGE default)** is what you want when tied rows must agree: every row dated 2024-01-02 showing the same end-of-day cumulative total is a legitimate, often desirable report. - **Row-by-row (ROWS)** is what you want when each row must show its own position in the accumulation, or when you need a deterministic per-row answer. ## Frames without an ORDER BY, and functions that ignore frames Some engines accept an explicit frame with no window `ORDER BY`; the result is unspecified because row order inside the partition is unspecified, so it is not a useful thing to write. `RANGE` with a numeric or interval offset, and `GROUPS` mode, require an `ORDER BY` outright. Finally, the frame does not affect every window function. Ranking functions (`ROW_NUMBER`, `RANK`, `DENSE_RANK`, `NTILE`, `PERCENT_RANK`, `CUME_DIST`) and the offset functions `LAG`/`LEAD` ignore the frame entirely. Aggregates used as window functions, and `FIRST_VALUE`/`LAST_VALUE`/`NTH_VALUE`, are the ones whose answers move when you change the frame — those are the calls where the default matters.
- If the window ORDER BY key is unique across the whole partition, does the default frame still differ from ROWS?No. With a unique ordering key every row is its own peer group, so "through my last peer" and "through me" select the same rows. The default and `ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW` then return identical values. That is why the trap only shows up on repeatable keys such as a date column with several rows per day.
- Which window functions are unaffected by whichever default frame applies?Ranking functions — `ROW_NUMBER`, `RANK`, `DENSE_RANK`, `NTILE`, `PERCENT_RANK`, `CUME_DIST` — and the offset functions `LAG` and `LEAD` ignore the frame clause; they are defined over the ordered partition itself. Aggregates used with `OVER`, plus `FIRST_VALUE`, `LAST_VALUE` and `NTH_VALUE`, do read the frame, so those are where a default silently changes the answer.
- How do you write the shorthand for the whole partition when the window already has an ORDER BY?Spell out both bounds: `ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING` (or the RANGE equivalent). Once `ORDER BY` is present the implicit whole-partition frame is gone, so a function that must see every row of the partition — a partition total shown next to an ordered detail listing — needs the frame written out explicitly.
The default frame is like a cashier who totals a queue by group rather than by person: everyone arriving at the same moment is rung up together, so all of them are handed the same subtotal.
saying these in an interview costs you the question
- Says ORDER BY inside OVER only sorts and cannot change values
- Assumes the default frame is ROWS, not RANGE
- Thinks omitting the frame means no frame at all
- Believes a running total always accumulates one row at a time
- Cannot say what the frame is when OVER has no ORDER BY