skip to content

A share-of-total column using SUM() OVER () shows shares of a filtered subset, not the company total — why?

level: seniorimportance: should knowfreq 40%

answer

  1. ask which rows the OVER clause could see
  2. the predicate ran before the window did
  3. move the filter to a different query level
  4. widening the frame cannot bring rows back

basics

~20 s

Because WHERE is applied before window functions: the window only ever sees rows that survived the filter, so its total covers the filtered subset. To divide by an unfiltered total, compute the window in an inner query over all rows and filter outside it.

solid answer

~50 s

The window's input is whatever the query's earlier clauses left behind. `WHERE region = 'EAST'` removes every other region before any `OVER` clause is evaluated, so `SUM(amount) OVER ()` is the EAST total and the percentages add up to 100% *of EAST*, not of the company. Nothing errors — you get a plausible wrong number. There are two clean fixes. Put the window in an inner query over the unfiltered rows and apply the predicate in the outer query, so the denominator is computed before the rows are narrowed. Or compute the company total once in a CTE and `CROSS JOIN` it, which keeps the filtered query small when the detail set is large. Widening the frame does **not** help: a frame only reshapes the window within a partition of rows the query already has.

code

sql · 12 lines
sql
-- Wrong: SUM() OVER () sees only EAST rows, so this is share-of-EAST
SELECT region, rep, amount,
       100.0 * amount / SUM(amount) OVER () AS pct_of_company
FROM sales
WHERE region = 'EAST';

-- Right: window over the full set, predicate applied one level up
SELECT *
FROM (SELECT region, rep, amount,
             100.0 * amount / SUM(amount) OVER () AS pct_of_company
      FROM sales) s
WHERE region = 'EAST';

go deeper

for a junior

Know that a WHERE clause changes what a window function totals, and that a percentage column is only as trustworthy as the row set the query kept.

for a middle

Explain the cause from the logical pipeline and write the wrapping rewrite: window computed in an inner query over all rows, predicate applied in the outer query.

for a senior

Diagnose from the query text without running it, offer both rewrites with the tradeoff between them, and add the invariant check that would have caught the wrong denominator before it reached a dashboard.

for a principal

Decide where denominators live for the organisation — a shared view or semantic layer that defines company total once — so that individual dashboards cannot each invent a filter-sensitive version of the same number.

## The symptom A dashboard column claims to show each rep's share of company revenue. The finance report says the East region is 55% of the company; the dashboard shows East reps' shares adding to 100%. Nobody's arithmetic is wrong — the two queries are dividing by different denominators. ```sql -- The dashboard query SELECT region, rep, amount, 100.0 * amount / SUM(amount) OVER () AS pct_of_company FROM sales WHERE region = 'EAST'; ``` ## The cause SQL evaluates `WHERE` before window functions. By the time `SUM(amount) OVER ()` runs, the only rows in existence at that query level are East rows, so the window totals East. The column is a correct share-of-East wearing a misleading alias. This is the single most common way an evaluation-order misunderstanding shows up in production: the query is syntactically fine, runs fast, returns numbers of the right magnitude, and is wrong. `HAVING` does the same thing one stage later in a grouped query — any group it removes is absent from a grand total computed by a window. ## Fix 1 — window inside, filter outside Move the filter to a query level *above* the window, so the window still sees the whole table. ```sql SELECT * FROM (SELECT region, rep, amount, 100.0 * amount / SUM(amount) OVER () AS pct_of_company FROM sales) s WHERE region = 'EAST'; ``` Or the same thing as a CTE, which reads better in a long query: ```sql WITH scored AS ( SELECT region, rep, amount, 100.0 * amount / SUM(amount) OVER () AS pct_of_company FROM sales ) SELECT * FROM scored WHERE region = 'EAST'; ``` Now `pct_of_company` means what it says. The cost is honest and stated in the SQL: you have asked for an aggregate over every row of `sales`, so the query reads the whole table even though it returns a handful of rows. ## Fix 2 — compute the denominator separately and cross join When the detail set is large and you only need one scalar, ask for exactly that: ```sql WITH company AS ( SELECT SUM(amount) AS company_total FROM sales ) SELECT s.region, s.rep, s.amount, 100.0 * s.amount / c.company_total AS pct_of_company FROM sales s CROSS JOIN company c WHERE s.region = 'EAST'; ``` `company` is a one-row set, so the `CROSS JOIN` attaches the same scalar to each detail row without changing the row count. A scalar subquery in the select list expresses the same idea. This form also makes the denominator's own scope explicit and independently editable — you can give it a date range that differs from the outer filter, which the wrapping form cannot do. ## What does not fix it - **Widening the frame.** `ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING` describes which rows *within the partition* the window covers. The partition is drawn from rows the query already holds; a frame cannot resurrect rows `WHERE` discarded. - **Adding or removing `PARTITION BY`.** Partitioning subdivides the surviving rows. Removing it gives you all surviving rows — still only East ones. - **Swapping `WHERE` for `HAVING`.** In a grouped query, `HAVING` still runs before the window, so the removed groups are still missing from the total. It also changes what the query means. ## Choosing between the fixes Wrap-and-filter is the smaller edit and keeps a single row source, so it is the default. Prefer the CTE-plus-`CROSS JOIN` shape when the denominator's scope genuinely differs from the detail rows' scope (company total for the year, detail rows for the month), or when the detail table is large enough that computing a per-row window across all of it is work you would rather not ask for. Whichever you choose, **name the column honestly** — `pct_of_company` versus `pct_of_region` — because the alias is the only thing standing between the next reader and the same misunderstanding. ## Verifying the fix Check the invariant the number must satisfy. If the column claims to be a share of the company, the shares of *all* rows across *all* regions must add to 100, so run the query without the filter and sum it. If it claims to be a share of the region, the shares within one region must add to 100. Sanity checks like this catch the whole family of denominator bugs and cost one extra query. ## What an interviewer is checking That you diagnose from the pipeline rather than by trial and error, that you can name at least two correct rewrites and say when each is preferable, and that you reject the frame-widening "fix" for the right reason.

  • What is the cost of moving the filter to an outer query around the window?
    You are now asking for an aggregate over every row of the table, so the statement reads far more data than the filtered version even though it returns the same few rows. When only one scalar denominator is needed, computing that total in its own CTE and cross joining it asks for less.
  • Would ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING fix the denominator?
    No. A frame chooses which rows inside the current partition contribute, and the partition is built from rows the query still has. WHERE already removed the others, so the widest possible frame still totals only the filtered set.
  • How would you compute a share of the company total while showing only one region and only this month's rows?
    Give the denominator its own scope in a CTE — for example the company total for the whole year — and cross join that one-row set to the filtered detail query. The wrapping rewrite cannot do this, because it forces the denominator to share the detail query's row source.
  • How do you check quickly whether a percentage column has the right denominator?
    Test the invariant. Remove the filter and sum the column across all rows: a genuine share-of-company must total 100. Summing to 100 within a single region instead tells you the denominator is region-scoped whatever the alias claims.

saying these in an interview costs you the question

  • Says SUM() OVER () always covers the whole table
  • Tries to fix it by widening the window frame
  • Claims PARTITION BY can reach rows removed by WHERE
  • Replaces WHERE with HAVING to dodge the filter
  • Believes the window is computed before WHERE

context