GROUP BY vs HAVING in SQL: What's the Difference
GROUP BY collapses rows into groups for aggregation; HAVING filters those groups afterward. How the two clauses differ and where each runs in query order.
GROUP BY collapses rows that share a value into groups so an aggregate function can summarize each group; HAVING filters those already-formed groups based on the aggregate result. They’re often used together, but they run at different stages of query execution and answer different questions — GROUP BY decides how rows are bucketed, HAVING decides which buckets survive.
Query execution order
SQL reads like SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY, but that’s not the order it actually runs in. The logical execution order is:
FROM— identify the source rows, including any joins.WHERE— filter individual rows, before any grouping happens.GROUP BY— collapse the remaining rows into groups by the specified column(s).- Aggregate functions (
COUNT,SUM,AVG, etc.) — computed once per group. HAVING— filter groups based on the aggregate results.SELECT— choose which columns/expressions to return.ORDER BY— sort the final result set.
This ordering explains most of the confusion between WHERE and HAVING: by the time HAVING runs, individual rows no longer exist as such — only groups and their aggregate values do.
GROUP BY: collapsing rows into groups
Given an orders table, this groups rows by customer and counts them:
SELECT customer_id, COUNT(*) AS order_count
FROM orders
GROUP BY customer_id;
Every column in the SELECT list must either appear in GROUP BY or be wrapped in an aggregate function — a database can’t return a single row per group and also return a column with a different value for every row inside that group. Some databases enforce this strictly at query-parse time; others (notably older MySQL configurations) historically allowed it and picked an arbitrary row’s value, which is a common source of subtly wrong results.
HAVING: filtering after aggregation
HAVING applies a condition to the aggregated result, after grouping has already happened:
SELECT customer_id, COUNT(*) AS order_count
FROM orders
GROUP BY customer_id
HAVING COUNT(*) > 5;
This returns only customers with more than five orders. Critically, you can’t write WHERE COUNT(*) > 5 — WHERE runs before grouping, when there’s no aggregate value to compare yet, since COUNT(*) only means something once rows have been collapsed into a group.
WHERE vs HAVING
WHERE | HAVING | |
|---|---|---|
| Runs | Before grouping | After grouping and aggregation |
| Filters | Individual rows | Groups (based on aggregate values) |
| Can reference aggregates | No | Yes |
| Typical use | Excluding rows before they’re grouped | Excluding groups whose aggregate fails a condition |
| Performance | Reduces the row count before the more expensive grouping step | Runs on an already-reduced set of groups |
A well-optimized query filters as much as possible in WHERE, before grouping, since that reduces the number of rows the database has to aggregate in the first place. HAVING should only carry conditions that genuinely depend on the aggregate result — anything that could be expressed as a per-row filter belongs in WHERE instead.
Combining both in one query
The two clauses are frequently combined, with WHERE narrowing the input and HAVING narrowing the output:
SELECT customer_id, COUNT(*) AS order_count
FROM orders
WHERE order_date >= '2026-01-01'
GROUP BY customer_id
HAVING COUNT(*) > 5
ORDER BY order_count DESC;
This reads as: consider only 2026 orders, group what’s left by customer, keep only customers with more than five qualifying orders, and sort by order count. Each clause does exactly one job, in the order the database actually evaluates them.
Common mistakes
- Putting an aggregate condition in
WHERE. It will fail (or, on some databases, error out) because the aggregate doesn’t exist yet at that stage. - Forgetting non-aggregated columns need to be in
GROUP BY. Selecting a column that isn’t grouped or aggregated is ambiguous — which row’s value should the database pick for a group with multiple rows? - Using
HAVINGwhereWHEREwould be cheaper. Filtering rows out early withWHEREavoids aggregating data that will just get discarded byHAVINGafterward. - Expecting
HAVINGto filter on a column alias defined inSELECT. Some databases allow referencing aSELECTalias inHAVING; others require repeating the full aggregate expression, sinceSELECTruns afterHAVINGin logical order. Check your database’s specific behavior, or repeat the expression to be safe.
GROUP BY/HAVING queries are also where a query planner’s choices matter most — running EXPLAIN ANALYZE on a slow aggregation will usually show whether the database is scanning far more rows than necessary before it ever reaches the grouping step, which is often better solved with an index than by restructuring the HAVING clause. For queries that need per-row context alongside an aggregate rather than one row per group, a window function is often the better tool — it computes aggregates without collapsing rows the way GROUP BY does. A CTE can also help by pre-filtering or pre-aggregating a subset of data before the main GROUP BY runs, keeping each part of a complex query readable on its own.
The takeaway
GROUP BY decides how rows collapse into groups; HAVING decides which of those groups make it into the result. WHERE filters rows before grouping and can’t see aggregate values; HAVING filters groups after aggregation and can. Push as much filtering as possible into WHERE for performance, reserve HAVING for conditions that genuinely depend on an aggregate, and reach for a window function instead when you need aggregate context without losing the individual rows.
Tagged
Keep reading
The Lycoris Team · · 5 min read PostgreSQL Full-Text Search Explained: tsvector and tsquery
PostgreSQL's built-in full-text search uses tsvector documents and tsquery queries matched with @@, indexed with GIN for speed at scale.
The Lycoris Team · · 4 min read SQL CTEs vs Subqueries: When to Use Which
Common table expressions and subqueries both let you build a query from smaller pieces, but they differ in readability, reuse, and optimizer behavior.
The Lycoris Team · · 4 min read Natural Keys vs Surrogate Keys in Database Design
Natural keys use real-world data as a primary key; surrogate keys use a generated ID. Here's how to choose, with the trade-offs of each.