The logical order HAVING has to obey
SQL evaluates a query in a fixed logical order that's different from the order you type it in: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY. Each stage consumes the output of the one before it. By the time HAVING runs, GROUP BY has already collapsed the surviving rows into groups and computed any aggregate functions — which is exactly the input HAVING needs to do its job. The full breakdown of every stage is in SQL Execution Order Explained.
What HAVING actually filters
HAVING operates on the result of GROUP BY — one row per group, with any aggregate columns already calculated. It cannot see individual pre-grouped rows at all; by the time it runs, those rows have already been folded into their groups.
SELECT customer_id, COUNT(*) AS order_count
FROM orders
GROUP BY customer_id
HAVING COUNT(*) > 5;
This returns only the customers whose order count — a value that only exists after grouping — is greater than 5. There is no way to express this condition in WHERE, because WHERE never sees a completed count; it only sees one row at a time, before any grouping has occurred.
WHERE vs HAVING, side by side
The cleanest way to see the difference is a query that legitimately needs both — one condition on raw rows, one on the aggregated result. For the full three-way comparison including SELECT, see SELECT vs WHERE vs HAVING.
SELECT customer_id, SUM(total) AS lifetime_value
FROM orders
WHERE status = 'completed' -- row filter, runs first
GROUP BY customer_id
HAVING SUM(total) > 1000 -- group filter, runs after
ORDER BY lifetime_value DESC;
| WHERE | HAVING | |
|---|---|---|
| Filters | Individual rows | Groups (after GROUP BY) |
| Can reference aggregates? | No | Yes — that's its purpose |
| Runs | Before GROUP BY | After GROUP BY |
| Works without GROUP BY? | Yes — always | Yes — treats the whole table as one group |
The common mistake: row filters in HAVING
Because HAVING looks like a second WHERE, it's tempting to put every condition there — including ones that don't need an aggregate at all. This is a recognized anti-pattern covered in GROUP BY Mistakes That Break SQL Queries.
-- Works, but wasteful: groups every row, including
-- rows HAVING is about to throw away anyway
SELECT customer_id, COUNT(*)
FROM orders
GROUP BY customer_id
HAVING customer_id IS NOT NULL;
-- Better: eliminate the row before grouping, not after
SELECT customer_id, COUNT(*)
FROM orders
WHERE customer_id IS NOT NULL
GROUP BY customer_id;
Can HAVING be used without GROUP BY?
Yes — a query with no GROUP BY is logically treated as one single group containing every surviving row, so HAVING still has a group to filter, just one instead of many.
SELECT COUNT(*) AS total_orders
FROM orders
HAVING COUNT(*) > 100;
-- Returns one row if there are more than 100 orders total,
-- otherwise returns nothing at all
Why WHERE-then-HAVING is usually faster
WHERE runs before grouping, so rows it eliminates never get counted, summed, or grouped at all. A condition placed in HAVING instead has to wait until every row has already been grouped and aggregated, only to throw part of that work away afterward. Pushing row-level conditions into WHERE whenever possible is one of the most reliable wins in the SQL Optimization Checklist.
Key takeaways
- HAVING comes after GROUP BY because it filters on aggregate values that GROUP BY produces.
- WHERE filters rows before grouping; HAVING filters groups after grouping.
- HAVING works even without GROUP BY — the whole result set is treated as one group.
- A row-level condition belongs in WHERE, not HAVING, even though HAVING would technically work.
- Filtering early with WHERE reduces how much data GROUP BY has to aggregate in the first place.