the difference that decides aggregations
Treating HAVING and WHERE as interchangeable filter clauses produces queries that either work by accident or run needlessly slow. The difference lies in execution order: WHERE filters before aggregation, HAVING after, and that is exactly what decides what can be filtered at all.
Table of Contents
- 1. Why HAVING vs. WHERE is not a style question
- 2. The logical execution order of SQL
- 3. WHERE: what is possible before aggregation
- 4. HAVING: filtering on aggregate functions
- 5. Combining WHERE and HAVING
- 6. Performance difference: early vs. late filtering
- 7. HAVING without GROUP BY: the special case
- 8. Common mistakes in practice
- 9. HAVING and WHERE compared directly
- 10. Summary
- 11. FAQ
1. Why HAVING vs. WHERE is not a style question
Many SQL introductions present the difference between HAVING vs. WHERE as a pure syntax rule: WHERE comes before GROUP BY, HAVING after. That is syntactically correct, but it obscures the actual reason two separate clauses exist. WHERE and HAVING operate at different stages of query processing, and that stage determines which expressions are even valid.
WHERE filters individual rows before any grouping or aggregation has taken place. HAVING filters groups after aggregate functions such as SUM, COUNT, or AVG have already been computed. This order is not an arbitrary language convention but reflects exactly how a relational database processes a query internally. Understanding HAVING vs. WHERE from this angle avoids both syntax errors and needlessly slow queries.
The practical difference becomes especially clear as soon as a filter should apply to an aggregate function, for instance all customers with more than ten orders. This filter cannot live in WHERE, because at the time WHERE is evaluated, orders per customer have not yet been counted. That is exactly the case HAVING exists for.
2. The logical execution order of SQL
SQL is written in a certain order, FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, but evaluated internally in a different order. First the data source is built in FROM, including all joins. Then WHERE filters individual rows out of that data source. Only after that does GROUP BY group the remaining rows, and HAVING filters the resulting groups. SELECT chooses the output columns, and ORDER BY sorts the final result.
This logical order explains why HAVING vs. WHERE are not interchangeable alternatives. An expression in WHERE can only reference columns of the base tables, never the result of an aggregate function, because that simply does not exist yet at the time WHERE is evaluated. HAVING, on the other hand, can access both aggregate functions and, in most databases, the grouping columns themselves.
-- Logical execution order (not the written order):
-- 1. FROM (build the source, including joins)
-- 2. WHERE (filter individual rows)
-- 3. GROUP BY (group remaining rows)
-- 4. HAVING (filter the resulting groups)
-- 5. SELECT (choose output columns)
-- 6. ORDER BY (sort the final result)
SELECT
customer_id,
COUNT(*) AS order_count,
SUM(amount) AS total_revenue
FROM orders
WHERE order_date >= '2026-01-01' -- filters rows before grouping
GROUP BY customer_id
HAVING COUNT(*) > 10 -- filters groups after aggregation
ORDER BY total_revenue DESC;
A common misunderstanding is the assumption that ORDER BY can reference an alias computed in HAVING directly, without the database re-evaluating it. In most databases this actually works, because SELECT aliases may be reused in ORDER BY, but not in WHERE or HAVING itself, since these clauses are logically evaluated before SELECT.
3. WHERE: what is possible before aggregation
WHERE operates exclusively at row level and knows nothing about groups. Every condition in WHERE must reference column values of a single row, as it comes directly from the table or a join. Typical WHERE conditions are date filters, status filters, or text comparisons that can be decided individually for each row regardless of a later grouping.
The decisive advantage of WHERE over HAVING: rows excluded by WHERE are never aggregated. That means less data volume for the subsequent GROUP BY processing and therefore usually a faster query. A WHERE filter on an indexed date can also use an index scan, which would no longer be possible for an equivalent HAVING condition on the same date, because by then the date is already part of an aggregated group.
-- WHERE filters rows BEFORE aggregation — cheap and index-friendly
SELECT
product_category,
COUNT(*) AS sale_count,
SUM(quantity) AS total_quantity
FROM sales
WHERE sale_date >= '2026-06-01'
AND status = 'completed'
GROUP BY product_category;
-- Cannot be done in WHERE, since COUNT(*) does not exist yet at this stage:
-- WHERE COUNT(*) > 100 -- syntax error in every major database
4. HAVING: filtering on aggregate functions
HAVING exists exactly for the case where a filter refers to the result of an aggregate function, not to individual row values. Typical examples are customers with more than a certain number of orders, product categories with revenue above a threshold, or regions with an average order size below a limit. All these conditions can only be formulated after SUM, COUNT, or AVG has been computed per group.
HAVING can reference both aggregate functions and, in most databases including PostgreSQL and MySQL, the grouping columns themselves. A filter like HAVING region = 'North' AND SUM(revenue) > 10000 is perfectly valid, even though the plain column condition region = 'North' could just as well, and more efficiently, live in WHERE, since it requires no aggregate function.
-- HAVING filters on the result of aggregate functions
SELECT
region,
category,
SUM(revenue) AS total_revenue,
AVG(order_value) AS avg_order_value
FROM sales
GROUP BY region, category
HAVING SUM(revenue) > 10000
AND AVG(order_value) < 500;
-- HAVING can also reference the grouping column directly,
-- but a plain column condition belongs in WHERE for performance reasons
An often overlooked point: HAVING can reference a SELECT alias if the database supports it, for instance PostgreSQL and MySQL, while SQL Server and Oracle are stricter and require the full expression to be repeated in HAVING. This inconsistency between databases is a good reason to repeat the full aggregate expression in HAVING for portable SQL, rather than relying on alias support.
5. Combining WHERE and HAVING
In practice, WHERE and HAVING are almost always used together, each for the part of the problem it was built for. WHERE first reduces the data volume to the relevant rows, for instance a certain time range or a certain status. After that, GROUP BY groups the remaining rows, and HAVING filters the groups by the aggregated result. This combination is the normal case in nearly every reporting query.
A common mistake is accidentally placing a condition that actually belongs in WHERE into HAVING instead, because it sits in the same query next to a genuine HAVING condition. The result is often still correct, but performance suffers, since all rows are aggregated first before the actually early filter takes effect. The rule of thumb: anything that refers purely to row values belongs in WHERE, anything that needs an aggregate function belongs in HAVING.
-- WRONG: row-level condition placed in HAVING — filters after aggregation
SELECT region, SUM(revenue) AS total
FROM sales
GROUP BY region
HAVING region IN ('North', 'South') AND SUM(revenue) > 5000;
-- RIGHT: row-level condition moved to WHERE — filters before aggregation
SELECT region, SUM(revenue) AS total
FROM sales
WHERE region IN ('North', 'South')
GROUP BY region
HAVING SUM(revenue) > 5000;
6. Performance difference: early vs. late filtering
The performance difference between HAVING vs. WHERE follows directly from the execution order. A WHERE filter reduces the row count before the expensive aggregation even begins. A HAVING filter, in contrast, is only applied after all rows have been grouped and aggregated, even if many groups are discarded again at the end. On large tables, this difference can significantly change the execution plan.
Modern query optimizers can automatically rewrite some row-level conditions from HAVING into an equivalent WHERE, known as predicate pushdown. You should not rely on this, however, since this optimization does not apply in every database or for every expression. An explicit WHERE filter is always the safer and more readable choice, regardless of whether the optimizer would perform the rewrite anyway.
On very large fact tables in a reporting context, the difference between an early WHERE filter on an indexed date and a late, equivalent HAVING filter can mean several seconds of execution time, especially when the grouping itself is computationally expensive, for instance with many distinct grouping keys.
7. HAVING without GROUP BY: the special case
A lesser known case: HAVING can also be used without an explicit GROUP BY. In that case, the database treats the entire result set as a single group, and HAVING filters whether this one group is included in the result at all. This is useful for checking whether an aggregate condition is met across the entire table, for instance whether total revenue exceeds a threshold.
-- HAVING without GROUP BY: the whole result set is treated as one group
SELECT SUM(revenue) AS total_revenue
FROM sales
WHERE sale_date >= '2026-01-01'
HAVING SUM(revenue) > 1000000;
-- Returns either one row (condition met) or zero rows (condition not met)
This special case is used rarely, but is practical in monitoring or alerting queries, where an empty result serves as a signal that a threshold was not reached. Application code that checks for an empty result instead of comparing a numeric value can be built more simply this way, especially in systems that only evaluate the presence status of a query anyway.
8. Common mistakes in practice
The most common mistake with HAVING vs. WHERE is trying to use an aggregate function in WHERE, which leads to a syntax error in every relational database, since the aggregate function simply does not exist at the time WHERE is evaluated. The second common mistake is the opposite: a plain column condition is placed in HAVING, which is syntactically correct but costs unnecessary performance, since filtering only happens after aggregation.
A third, subtler mistake concerns NULL values in aggregate functions. HAVING SUM(column) > 0 behaves unexpectedly if all values in a group are NULL, since SUM returns NULL in that case, not 0, and a comparison with NULL in SQL is neither true nor false, but unknown. The group is then silently removed from the result, which leads to hard to trace missing rows without knowledge of NULL semantics.
| Criterion | WHERE | HAVING |
|---|---|---|
| Level of effect | Individual rows, before aggregation | Groups, after aggregation |
| Aggregate functions allowed | No | Yes |
| Usable without GROUP BY | Yes, standard case | Yes, whole table as one group |
| Index usage | Directly possible | Not on aggregate result |
| Performance effect | Reduces data early | Filters after expensive aggregation |
Mironsoft
SQL reporting, data modeling, and query optimization
Slow reporting queries with misplaced filters?
We review existing queries for misplaced WHERE and HAVING conditions and build reporting SQL that filters as early as possible and aggregates only afterward.
Query review
Check HAVING and WHERE conditions for efficiency
Performance tuning
Analyze execution plans and place filters early
Reporting setup
Design new dashboards with clean aggregation queries
10. Summary
The difference between HAVING vs. WHERE follows directly from the logical execution order of SQL: WHERE filters rows before GROUP BY groups them and aggregate functions are computed, HAVING filters the resulting groups afterward. Aggregate functions such as SUM, COUNT, and AVG can therefore only be referenced in HAVING, never in WHERE. Plain column conditions, on the other hand, always belong in WHERE, even if they could syntactically also live in HAVING, because there they delay aggregation unnecessarily.
Combining both clauses is the normal case in reporting queries: WHERE reduces the data volume early, HAVING filters the aggregated result by business criteria such as minimum revenue or minimum order count. Consistently maintaining this separation produces queries that are both correct and performant, regardless of the underlying database.
HAVING vs. WHERE — the essentials at a glance
WHERE
Filters individual rows before aggregation, no aggregate functions allowed, uses indexes directly.
HAVING
Filters groups after aggregation, aggregate functions and grouping columns allowed.
Rule of thumb
Plain column condition: WHERE. Condition on an aggregate function: HAVING.
NULL trap
SUM() over only NULL values returns NULL, not 0, a comparison in HAVING then fails silently.