SQL ยท easy

What is the difference between WHERE and HAVING?

Asked in BI Developer interviews, in the SQL round.

Short answer

WHERE filters rows before aggregation, HAVING filters groups after GROUP BY.

WHEREHAVING
RunsBefore GROUP BYAfter GROUP BY
FiltersIndividual rowsGroups
Can use SUM, COUNT, AVGNoYes
Can use a column aliasNoYes, in most engines
PerformanceCheaper, cuts rows earlyRuns on the aggregated set
WHERE filters rows on the way in, HAVING filters groups on the way out.

How to answer it

Definition

WHERE filters individual rows before they are grouped. HAVING filters the groups that GROUP BY produced, after aggregation has run. Both narrow a result set; they simply do it at two different moments, and that is the whole difference.

A condition on a raw column belongs in WHERE. A condition on an aggregate such as COUNT(*) or SUM(amount) belongs in HAVING, because the aggregate does not exist yet at the moment WHERE runs.

How SQL actually runs the clauses

A query is not executed in the order it is written. The logical order is:

1. FROM       pick the tables and join them
2. WHERE      drop rows that fail the condition
3. GROUP BY   fold the surviving rows into groups
4. HAVING     drop groups that fail the condition
5. SELECT     compute the output columns and their aliases
6. ORDER BY   sort what is left
7. LIMIT      cut the result

Nearly every question about these two clauses is answered by that list. WHERE is step 2 and cannot see an aggregate, because aggregates are not formed until step 3. HAVING is step 4 and can see them. An alias invented at step 5 cannot be used back at step 2.

Syntax

SELECT   column, AGGREGATE(column)
FROM     table
WHERE    row_condition                    -- before grouping
GROUP BY column
HAVING   AGGREGATE(column) condition      -- after grouping

Example: filter rows, then filter groups

SELECT   customer_id, COUNT(*) AS orders_2025
FROM     orders
WHERE    order_date >= '2025-01-01'
GROUP BY customer_id
HAVING   COUNT(*) >= 5;

WHERE throws away every order placed before 2025. GROUP BY folds what survives into one row per customer. HAVING then keeps only the customers with five or more of those remaining orders. Note that the two filters cannot be swapped: a customer with twenty orders in 2024 and one in 2025 is excluded, because the row filter ran first.

Output

customer_id | orders_2025
------------+-------------
        101 |           7
        204 |           5

HAVING without GROUP BY

Legal, and occasionally useful. With no GROUP BY, the entire result is treated as a single group:

SELECT SUM(amount) AS revenue
FROM   orders
HAVING SUM(amount) > 1000000;

That returns one row if total revenue clears a million, and no rows at all otherwise.

Common errors

Putting an aggregate in WHERE:

-- wrong
SELECT   customer_id
FROM     orders
WHERE    COUNT(*) >= 5
GROUP BY customer_id;

Postgres replies aggregate functions are not allowed in WHERE. The fix is to move the condition into HAVING.

Using a SELECT alias in WHERE:

-- wrong
SELECT amount * 0.2 AS tax FROM orders WHERE tax > 100;

SELECT runs after WHERE, so tax does not exist yet. Repeat the expression in WHERE, or wrap the query in a subquery or CTE.

Which one to use

Performance

WHERE does strictly less work. It removes rows before the grouping step, so the engine sorts, hashes and aggregates a smaller set. A condition pushed from HAVING down into WHERE can also let the planner use an index on that column, which HAVING never can, because by then the original rows are gone.

On a large table the difference is not academic. Filtering a year of orders down to one month in WHERE means grouping a twelfth of the rows.

Key points

FAQ

The 30-second answer

WHERE filters rows before they are grouped, HAVING filters the groups after. So a condition on a raw column, like a date range, goes in WHERE. A condition on an aggregate, like having at least five orders, has to go in HAVING, because that count does not exist yet at the point WHERE runs.
The reason is the evaluation order: FROM, WHERE, GROUP BY, HAVING, then SELECT. That is also why an alias I define in SELECT is not available back in WHERE. And if a condition could sit in either clause, I would put it in WHERE, because it shrinks the set before the grouping has to do any work.

In the interview

Practice this for real