HAVING
HAVING filters the groups produced by a GROUP BY clause, based on the result of an aggregate function. It plays the same role for grouped results that WHERE plays for individual rows — the difference is entirely about when the filter is applied.WHERE filters before grouping, HAVING filters after
Clause | Filters | Runs |
|---|---|---|
| Individual rows | Before grouping and aggregation |
| Groups (via an aggregate condition) | After grouping and aggregation |
Because
WHERE runs before the rows have even been grouped, it has no access to a group's aggregate value yet — COUNT(*), SUM(total), and similar expressions simply don't exist at that stage of execution. HAVING runs after aggregation, so it can reference those aggregate results directly.A side-by-side example
Suppose you want to find customers who have placed more than 5 orders. It's tempting to reach for
WHERE:This fails
SQL
SELECT customer_id, COUNT(*) AS order_count FROM orders WHERE COUNT(*) > 5 GROUP BY customer_id; -- ERROR: aggregate functions are not allowed in WHERE clause
PostgreSQL rejects this outright: at the point
WHERE is evaluated, rows haven't been grouped yet, so there is no COUNT(*) to compare against. Swap in HAVING and it works exactly as intended:This works
SQL
SELECT customer_id, COUNT(*) AS order_count FROM orders GROUP BY customer_id HAVING COUNT(*) > 5;
customer_id | order_count
------------+-------------
42 | 9
108 | 6
215 | 14Combining WHERE and HAVING
WHERE and HAVING are not mutually exclusive — a single query commonly uses both, each doing the job it is suited for. Filter out rows you never want considered at all with WHERE, then filter the resulting groups by their aggregate values with HAVING.orders
SQL
SELECT customer_id, COUNT(*) AS delivered_orders, SUM(total) AS revenue FROM orders WHERE status = 'delivered' -- filter rows first GROUP BY customer_id HAVING SUM(total) > 500 -- then filter groups ORDER BY revenue DESC;
Here
WHERE status = 'delivered' throws out cancelled and pending orders before any grouping happens, and HAVING SUM(total) > 500 then keeps only the customers whose delivered-order revenue passes that threshold.HAVING can reference columns not in the SELECT list
SQL
SELECT region, SUM(amount) AS total_sales FROM sales GROUP BY region HAVING AVG(amount) > 100 AND COUNT(*) >= 10;
Note
HAVING can use any aggregate expression, including ones that never appear in the final SELECT list — as shown above, filtering on AVG(amount) and COUNT(*) while only selecting SUM(amount).Tip
A useful mental shortcut: if the condition mentions a raw column (
status = 'delivered'), it belongs in WHERE. If it mentions an aggregate function (COUNT(*), SUM(total)), it belongs in HAVING.WHEREfilters individual rows before grouping; it cannot reference aggregate results.HAVINGfilters groups after aggregation; it can reference aggregate results.WHERE COUNT(*) > 5is a compile error;HAVING COUNT(*) > 5is correct.A single query can use both —
WHEREto trim rows,HAVINGto trim groups.