How does COUNT(*) FILTER (WHERE status = 'paid') differ from putting that predicate in the query's WHERE clause?
answer
- Two filtering scopes in one query
- Attaches to one aggregate, not the statement
- Other measures in the SELECT list unaffected
- Only one of them can erase a group
basics
~20 sFILTER restricts the rows one aggregate sees, leaving every other aggregate and the grouping untouched. A WHERE predicate removes rows from the whole query, so it constrains every aggregate at once and can erase entire groups.
solid answer
~40 s`FILTER (WHERE …)` is part of the aggregate-function syntax, so its predicate is scoped to that **single aggregate**. In `SELECT COUNT(*), COUNT(*) FILTER (WHERE status = 'paid') FROM orders` the first count still sees all rows and the second sees only paid ones — one scan, two differently-filtered measures. Move `status = 'paid'` into `WHERE` and the row disappears from the query entirely: the unfiltered count now also reports only paid orders, and any customer with no paid orders vanishes from a grouped result instead of showing a zero. Both predicates are evaluated per row before aggregation — `FILTER` is not a late, group-level test like `HAVING`. It is standard SQL, implemented by PostgreSQL and SQLite; MySQL, SQL Server and Oracle need the `CASE` rewrite.
code
sql · 6 lines-- One pass, two differently-scoped measures
SELECT customer_id,
COUNT(*) AS orders_total,
COUNT(*) FILTER (WHERE status = 'paid') AS orders_paid
FROM orders
GROUP BY customer_id;go deeper
Be able to read a SELECT list where each aggregate carries its own FILTER and say what each number counts. Knowing that the predicate belongs to that one aggregate is the whole ask at this level.
Explain the scope difference precisely: WHERE removes rows before grouping and affects every aggregate, FILTER trims one aggregate's input. Be ready to show a query where the two give different answers.
Show the failure mode you have actually seen in reports: someone pushes a measure's predicate into WHERE, the totals collapse and the zero-activity groups disappear. Mention that engine support varies and name the CASE fallback.
Frame it as reporting-contract discipline: one grouped query producing every measure with explicit per-measure predicates is easier to review and to keep consistent than several near-duplicate queries, but the portability cost has to be a conscious choice.
## The clause and where it goes The standard's aggregate-function syntax allows an optional *filter clause* after the argument list: ```sql COUNT(*) FILTER (WHERE status = 'paid') SUM(amount) FILTER (WHERE status = 'paid') ``` The predicate inside `FILTER (WHERE …)` is an ordinary row predicate. Rows for which it is TRUE are fed into that aggregate; rows for which it is FALSE or UNKNOWN are simply not fed into it. Nothing else about the query changes. ## Two different scopes A query can filter at three different scopes, and confusing them is the whole point of this question. - **`WHERE`** filters the query's rows. It runs before grouping, so it decides which rows exist at all: every aggregate in the SELECT list sees only survivors, group membership is computed from survivors, and rows it removes cannot come back. - **`FILTER`** filters *one aggregate's input*. The rows still exist in the query, still form groups, and still feed the other aggregates. - **`HAVING`** filters whole groups after aggregation, using aggregate results. So `WHERE` is subtractive for the entire statement; `FILTER` is subtractive for exactly one measure. ## Worked example ```sql SELECT customer_id, COUNT(*) AS orders_total, COUNT(*) FILTER (WHERE status = 'paid') AS orders_paid FROM orders GROUP BY customer_id; ``` A customer with five orders, two of them paid, produces `(5, 2)`. Rewrite it as ```sql SELECT customer_id, COUNT(*) AS orders_total, COUNT(*) AS orders_paid FROM orders WHERE status = 'paid' GROUP BY customer_id; ``` and the same customer produces `(2, 2)` — the "total" is no longer a total. Worse, a customer with five orders and *none* paid disappears from the result entirely, because none of their rows survived `WHERE` and so no group was formed for them. With `FILTER` that customer is still returned, as `(5, 0)`. ## What FILTER buys you Before the filter clause (and still, on engines that lack it) the usual way to compute several differently-filtered measures side by side was conditional aggregation with `CASE`, or several correlated subqueries or self-joins over the same table. `FILTER` expresses the intent directly and lets one grouping pass produce many measures: ```sql SELECT COUNT(*) AS total, COUNT(*) FILTER (WHERE status = 'paid') AS paid, COUNT(*) FILTER (WHERE status = 'refunded')AS refunded, SUM(amount) FILTER (WHERE status = 'paid') AS revenue FROM orders; ``` Each aggregate carries its own condition, and the reader does not have to decode a stack of `CASE` expressions to see what is being counted. ## Where it sits in evaluation Logically: the FROM/JOIN result is filtered by `WHERE`, groups are formed by `GROUP BY`, then each aggregate consumes the rows of its group — applying its own filter predicate as it goes — and finally `HAVING`, the SELECT list and `ORDER BY` run. Two consequences follow. First, the filter predicate may reference any column of the underlying rows, including columns that are not in the `GROUP BY` list, because it is evaluated per row. Second, it may **not** contain an aggregate or a window function, which would nest an aggregate inside an aggregate. ## NULL handling in the predicate The predicate follows normal three-valued logic: only TRUE admits a row. `FILTER (WHERE bonus > 0)` silently skips rows where `bonus` is NULL, exactly as the same predicate in `WHERE` would. ## Portability The filter clause is standard SQL. PostgreSQL implements it (since 9.4) and SQLite implements it for aggregate functions (since 3.30). MySQL, SQL Server and Oracle do not accept it; on those engines you write the equivalent `CASE` form — `COUNT(CASE WHEN status = 'paid' THEN 1 END)` and `SUM(CASE WHEN status = 'paid' THEN amount END)`. Check your engine's documentation rather than assuming; support is the one thing that changes between releases. ## Typical mistakes Treating `FILTER` and `WHERE` as interchangeable is the main one; the difference only shows up when a second, differently-filtered measure or an all-non-matching group exists. The second is expecting a group to disappear when nothing in it matches the filter — it does not; `COUNT` reports 0 and `SUM`, `AVG`, `MIN`, `MAX` report NULL for that measure. The third is assuming `FILTER` is a portable spelling you can use anywhere.
- Does FILTER change which groups appear in the result?No. Group membership is decided by the rows that survive WHERE, so a group whose rows all fail the filter still appears — its COUNT reports 0 and its SUM, AVG, MIN and MAX report NULL. Only WHERE (before grouping) or HAVING (after it) can remove a group.
- Can the FILTER predicate reference a column that is not in the GROUP BY list?Yes. It is evaluated per input row before aggregation, exactly like a WHERE predicate, so it may use any column of the underlying rows. It may not contain an aggregate or a window function, because that would nest an aggregate inside an aggregate.
- How does FILTER differ from HAVING?HAVING is a group-level test applied after aggregation, and it removes whole groups from the result. FILTER is a row-level test applied while one aggregate consumes its group's rows; it changes a single measure's value and never removes a group or a row.
WHERE is the bouncer at the club door — anyone he turns away is gone from the whole building. FILTER is a wristband check at one bar inside: it changes who gets served there, and nobody else in the room notices.
saying these in an interview costs you the question
- Says FILTER and WHERE are two spellings of the same thing
- Thinks FILTER removes rows from the query's result set
- Believes FILTER is applied after grouping, like HAVING
- Assumes every engine accepts FILTER (WHERE ...)
- Claims a group vanishes when no row matches its FILTER