In SQL's logical evaluation order, when are window functions computed relative to WHERE, HAVING and LIMIT?
answer
- ask which rows reach the OVER clause
- filters run first, slices run last
- one of WHERE and LIMIT changes the total
- after HAVING, before ORDER BY and FETCH
basics
~20 sWindow functions run after FROM, WHERE, GROUP BY and HAVING, so they see only rows that survived filtering, and before DISTINCT, ORDER BY and LIMIT/FETCH, so row limits never shrink what a window computes over.
solid answer
~40 sThe logical pipeline is `FROM/JOIN → WHERE → GROUP BY → HAVING → window functions → SELECT list and DISTINCT → ORDER BY → OFFSET/FETCH`. Two consequences matter in practice. First, **filters shrink the window's input**: a `WHERE` predicate removes rows before any `OVER` clause is evaluated, so a total or a rank describes the filtered set, not the table. Second, **the row limit does not**: `FETCH FIRST 10 ROWS ONLY` is applied last, so `COUNT(*) OVER ()` on a paged query returns the size of the whole filtered result — which is exactly how you get a page of rows and the total row count in one statement. Because windows are computed at this stage, the earlier clauses cannot reference their results; filtering on a window value requires wrapping the query.
code
sql · 9 lines-- WHERE runs first: the percentage is a share of PAID orders only.
-- FETCH FIRST runs last: it does NOT shrink SUM() OVER () or COUNT() OVER ().
SELECT order_id, amount,
100.0 * amount / SUM(amount) OVER () AS pct_of_paid,
COUNT(*) OVER () AS paid_orders_total
FROM orders
WHERE status = 'PAID'
ORDER BY amount DESC
FETCH FIRST 10 ROWS ONLY;go deeper
Memorise the position: after WHERE and GROUP BY, before ORDER BY and the row limit. Being able to say a WHERE clause changes a window total is enough at this level.
Recite the full pipeline and derive its consequences on demand, especially that FETCH FIRST or LIMIT never narrows a window computation, and that a window result cannot be referenced by an earlier clause.
Use the ordering diagnostically: given a query returning suspicious totals or percentages, point at the clause responsible and propose the wrapping rewrite that puts the window on the right row set.
Set expectations for reporting queries across the team: where paging totals come from, whether denominators should be filter-sensitive, and how those decisions get encoded in shared views or query templates rather than rediscovered per dashboard.
## The pipeline SQL defines the *meaning* of a query as a sequence of logical steps. Adding window functions, it reads: 1. `FROM` / `JOIN` — build the row source 2. `WHERE` — discard rows that fail the predicate 3. `GROUP BY` — collapse the survivors into groups 4. `HAVING` — discard groups that fail the predicate 5. **window functions** — evaluated over the rows produced by steps 1–4 6. `SELECT` list and `DISTINCT` — project, then deduplicate 7. `ORDER BY` — order the result 8. `OFFSET` / `FETCH FIRST` (or `LIMIT`) — take a slice This is a *semantic* model, not a description of how an engine physically executes anything; the engine may do the work in any order that yields the same answer. But every question about what a query *returns* is settled by this list. ## Consequence 1: filters shrink what the window sees Because `WHERE` runs at step 2 and windows at step 5, a window function never sees a filtered-out row. ```sql SELECT order_id, amount, SUM(amount) OVER () AS total_shown FROM orders WHERE status = 'PAID'; ``` `total_shown` is the total of paid orders, not of all orders. Usually that is precisely what you want — the totals describe the rows on screen. It becomes a bug only when you wanted a denominator that ignores the filter, and then the fix is to move the filter outside the window's query level (see the rewrite below). The same holds for `HAVING` in a grouped query: groups it removes are gone before the window runs, so a grand total computed with `SUM(SUM(x)) OVER ()` excludes them. ## Consequence 2: the row limit does not shrink what the window sees `ORDER BY` and the limit clause run at steps 7–8, *after* windows. ```sql SELECT order_id, amount, COUNT(*) OVER () AS matching_rows FROM orders WHERE status = 'PAID' ORDER BY order_date DESC FETCH FIRST 10 ROWS ONLY; ``` You get ten rows, and every one of them carries the count of **all** paid orders. This is a well-known and genuinely useful idiom: one statement returns both a page of data and the total needed to render "page 1 of 37", with no second `COUNT` query that could disagree with the first because the data moved in between. It also means a running total in a paged query is a running total over the whole filtered set, not restarted per page — which is what a paginated report normally wants, and a surprise if you assumed the limit applied first. ## Consequence 3: earlier clauses cannot reference a window result Since windows are evaluated at step 5, nothing at steps 2–4 can see their output. A predicate on a window value must therefore be applied at a later stage — conventionally by computing the window in an inner query (a subquery or CTE) and filtering in the outer one, where the value is an ordinary column. ```sql SELECT * FROM (SELECT order_id, amount, SUM(amount) OVER (PARTITION BY customer_id) AS cust_total FROM orders) t WHERE cust_total > 1000; ``` The inner query computes the window over all rows; the outer predicate then filters on the result. The same wrapping technique is what makes the total-ignores-the-filter case work: put the window in the inner query over the unfiltered rows and apply the predicate outside. ## Consequence 4: DISTINCT is applied after windows `SELECT DISTINCT` deduplicates the projected rows at step 6, so the window has already produced a value for every row. Adding `ROW_NUMBER()` to a `SELECT DISTINCT` list makes every projected row unique, and the `DISTINCT` then removes nothing. That is not a bug in the engine; it follows directly from the ordering. ## Where this sits relative to grouping Step 5 comes after step 3, which is why a window function in a grouped query operates on the grouped rows and may take an aggregate as its argument. And it is why the two mechanisms compose rather than compete: grouping decides how many rows exist, windowing decorates whatever rows are left. ## A checkable summary Ask two questions of any query with an `OVER` clause: - **Which rows reach the window?** Everything after `WHERE`, grouping and `HAVING`. If a predicate is in the same query level, it has already applied. - **Which clauses run after the window?** `DISTINCT`, `ORDER BY`, `OFFSET`/`FETCH`. They cannot change what the window computed; they only choose what you see of it. ## What an interviewer is checking That you can place windowing in the pipeline without hesitation, and that you can immediately derive the two practical results: filters change window values, limits do not.
- How do you return one page of rows and the total number of matching rows in a single query?Add COUNT(*) OVER () to the select list and keep the ORDER BY and FETCH FIRST clauses. Since the limit is applied after window functions, each returned row carries the count of the whole filtered result set, so the page and its total can never disagree the way two separate queries can.
- Does a running total restart when the query is paginated with OFFSET and FETCH?No. The window is computed over the full filtered, ordered set before any slice is taken, so page three shows running totals continuing from page two. If you wanted a per-page total you would have to compute it after slicing, in an outer query over the page.
- Why does SELECT DISTINCT with ROW_NUMBER() in the list remove no duplicates?DISTINCT is applied after window functions. ROW_NUMBER() gives each projected row a unique value, so no two rows are identical any more and DISTINCT has nothing to collapse. Deduplicate in an inner query, or use the row number itself as the deduplication device.
saying these in an interview costs you the question
- Says LIMIT reduces the rows a window function sees
- Thinks WHERE runs after the window is computed
- Claims window results are visible to WHERE in the same query
- Believes ORDER BY in the query changes what the window computed
- Says the pipeline describes physical execution order