skip to content

HAVING vs WHERE

WHERE filters rows before grouping; HAVING filters whole groups after aggregation, so only HAVING may reference aggregate results. Interviewers ask this constantly because it doubles as a test of the logical query-evaluation order from FROM to ORDER BY.

part ofSQLoverview, primer and where to startread it →
on this pageshow

questions

5

What is the difference between the WHERE clause and the HAVING clause in SQL?

level: juniorimportance: must knowfreq 88%

answer

  1. One filters rows, the other filters groups
  2. Think about which runs before GROUP BY
  3. Only one of them can see COUNT(*)
  4. Aggregates do not exist yet when rows are filtered

basics

~20 s

WHERE filters individual rows before grouping; HAVING filters whole groups after aggregation. Only HAVING may reference aggregate results such as COUNT(*) or SUM(amount), because those values exist only once rows have been collapsed into groups.

solid answer

~40 s

They filter different things at different stages. `WHERE` tests one row at a time, before `GROUP BY` runs, so rows it rejects never contribute to any aggregate. `HAVING` tests one group at a time, after aggregation, so it can compare `COUNT(*)`, `SUM(amount)` or `AVG(salary)` against a threshold. That is why `WHERE COUNT(*) > 1` is rejected: at WHERE time no group exists yet. `HAVING` may reference the grouping columns and aggregates — including aggregates that are not in the SELECT list — but not an ungrouped plain column, which has many values inside one group. A predicate that touches only grouping columns can go in either clause with the same result; put it in `WHERE`, because it is a row filter and it hands less data to the grouping step.

code

sql · 5 lines
sql
SELECT customer_id, SUM(amount) AS total_2026
FROM orders
WHERE order_date >= DATE '2026-01-01'   -- row filter, before grouping
GROUP BY customer_id
HAVING SUM(amount) > 1000;              -- group filter, after aggregation

go deeper

for a junior

Be able to state the one-line rule instantly: WHERE filters rows before grouping, HAVING filters groups after aggregating. Expect to be asked why COUNT(*) cannot appear in WHERE.

for a middle

Explain the mechanics: which stage each clause runs in, what each is allowed to reference, and why filtering rows in WHERE changes the aggregate values while HAVING only keeps or drops already-computed groups.

for a senior

Show judgment about predicate placement in real reports: keep row filters in WHERE for clarity and smaller grouping input, reserve HAVING for genuine aggregate thresholds, and be able to explain a wrong total caused by a misplaced predicate.

for a principal

Own the convention across a codebase: predicate placement is a readability and correctness contract, not a micro-optimization. Be ready to argue why relying on an engine to push a HAVING predicate down is not something the language promises.

## Two filters, two stages Every SELECT that aggregates offers two places to reject data, and they act on different objects. `WHERE` tests **rows**: each candidate row coming out of `FROM` is examined on its own, and any row whose predicate is not TRUE never reaches the grouping step. `HAVING` tests **groups**: after `GROUP BY` has collapsed the surviving rows into one group per distinct set of grouping-key values, each group is examined once, and groups whose predicate is not TRUE are dropped. The standard's logical evaluation order is FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY. Every practical difference between the clauses follows from those two positions: WHERE runs while only rows exist, HAVING runs once groups and their aggregate values exist. ## What each clause may reference `WHERE` sees the columns of the tables in `FROM`, one row at a time. It cannot contain an aggregate such as `COUNT(*)`, `SUM(amount)` or `AVG(salary)`; at that point nothing has been grouped, so there is nothing to aggregate over, and a conforming engine rejects the statement. `HAVING` may reference aggregate expressions over the group, the grouping columns listed in `GROUP BY`, literals, and correlated references from an enclosing query. It may **not** reference a plain column that is neither a grouping key nor wrapped in an aggregate, because such a column holds many different values inside one group and there is no single value to test. A useful consequence: `HAVING` can filter on an aggregate that never appears in the result. ```sql SELECT customer_id FROM orders GROUP BY customer_id HAVING SUM(amount) > 10000; -- SUM is tested but not returned ``` ## A worked example ```sql SELECT customer_id, SUM(amount) AS total_2026 FROM orders WHERE order_date >= DATE '2026-01-01' -- row test: drop older orders GROUP BY customer_id HAVING SUM(amount) > 1000; -- group test: drop small customers ``` Read it in stages. `WHERE` discards every order placed before 2026. `GROUP BY` makes one group per customer out of what remains. `SUM(amount)` is computed **only over the 2026 orders**. `HAVING` keeps the customers whose 2026 total exceeds 1000. Moving the date test into `HAVING` is either illegal (`order_date` is not a grouping key) or means something entirely different. ## The clauses change different things This is the part candidates most often miss: `WHERE` changes the aggregate **values**, because it changes which rows feed them. `HAVING` never changes a value — it only decides whether an already-computed group survives. So a filter moved from `WHERE` into an aggregate-based `HAVING` predicate does not merely reorder work; it can produce different numbers as well as a different set of rows. ## When the two are interchangeable If the predicate references only grouping columns, either placement returns the same rows: ```sql -- same result set both ways SELECT region, COUNT(*) FROM sales WHERE region <> 'EU' GROUP BY region; SELECT region, COUNT(*) FROM sales GROUP BY region HAVING region <> 'EU'; ``` Prefer `WHERE`. It states the intent — this is a row filter — and it gives the grouping step less input. Whether an engine internally rewrites the second form into the first is an optimizer matter, not a guarantee of the language. ## HAVING without GROUP BY `HAVING` does not require `GROUP BY`. Without one, everything that survived `WHERE` forms a single implicit group and `HAVING` is one test over that group's aggregates: the query returns either the single aggregate row or no rows at all. ## Three-valued logic applies to both Like `WHERE`, `HAVING` keeps only what evaluates to TRUE. If a group's aggregate is NULL — `SUM(x)` over a group whose `x` values are all NULL, for instance — then `SUM(x) > 0` evaluates to UNKNOWN and the group is discarded, exactly as an UNKNOWN row predicate discards a row. ## What interviewers listen for "HAVING is just WHERE for GROUP BY queries" misses that they filter different objects and see different data. `WHERE COUNT(*) > 1` is the classic error. Putting an ordinary row predicate in `HAVING` because "the query has a GROUP BY" is legal only for grouping columns and misleading even then. And expecting `HAVING` to surface empty groups never works: a group exists only because at least one row produced it, so `COUNT(*)` for any returned group is at least 1.

  • Can HAVING reference a column that is neither in GROUP BY nor inside an aggregate?
    No. Inside one group that column may hold many different values, so there is nothing single to compare. A conforming engine rejects it, the same way it rejects such a column in the SELECT list. Either add the column to GROUP BY, wrap it in an aggregate such as MIN() or MAX(), or move the predicate to WHERE where it applies per row.
  • If a predicate touches only a grouping column, does it matter whether you write it in WHERE or HAVING?
    The result set is identical, so it is a style and efficiency call rather than a semantic one. WHERE is the better choice: it declares that this is a row-level filter and it reduces the input to grouping. Reserve HAVING for predicates that genuinely need aggregate values, so a reader can tell the two intentions apart at a glance.
  • Can a single query use WHERE and HAVING together, and in what order do they apply?
    Yes, and that is the common shape. WHERE applies first, per row; GROUP BY then forms groups from the survivors; aggregates are computed over those rows only; HAVING then tests each group. So HAVING always evaluates aggregates of the WHERE-filtered data, never of the full table.

saying these in an interview costs you the question

  • Says HAVING is simply WHERE for grouped queries
  • Writes WHERE COUNT(*) > 1 to filter groups
  • Claims HAVING requires a GROUP BY clause
  • Thinks HAVING can test any column of the table
  • Believes moving a filter to HAVING never changes the numbers

context

open as a page

How would you use GROUP BY and HAVING to find duplicate email addresses in a users table?

level: juniorimportance: must knowfreq 78%

basics

~20 s

Group by the column and keep only the groups with more than one row: SELECT email, COUNT() FROM users GROUP BY email HAVING COUNT() > 1. WHERE cannot express this, because the count exists only after grouping.

open as a page

What does a HAVING clause do in a query that has no GROUP BY?

level: middleimportance: should knowfreq 35%

basics

~10 s

Without GROUP BY, every row surviving WHERE forms one implicit group, and HAVING is a single test over that group's aggregates. The query returns either the one aggregate row or no rows at all.

open as a page

In a per-department salary query, how does WHERE salary > 50000 differ from HAVING AVG(salary) > 50000?

level: middleimportance: should knowfreq 62%

basics

~20 s

WHERE drops low-paid employees before grouping, so a department appears if it has any high earner and its reported average covers only those employees. HAVING keeps everyone in the average and returns only departments whose overall average exceeds 50000.

open as a page

A 'customers with no orders' report uses HAVING COUNT(*) = 0 and returns nothing — why?

level: seniorimportance: should knowfreq 40%

basics

~20 s

GROUP BY creates a group only where rows exist, so a customer with no orders produces no group for HAVING to test. HAVING can only remove groups, never invent them, and every surviving group has COUNT(*) of at least 1.

open as a page