Articles

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.

The Lycoris Team The Lycoris Team · · 4 min read
An abstract illustration of stacked database tables

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:

  1. FROM — identify the source rows, including any joins.
  2. WHERE — filter individual rows, before any grouping happens.
  3. GROUP BY — collapse the remaining rows into groups by the specified column(s).
  4. Aggregate functions (COUNT, SUM, AVG, etc.) — computed once per group.
  5. HAVING — filter groups based on the aggregate results.
  6. SELECT — choose which columns/expressions to return.
  7. 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

WHEREHAVING
RunsBefore groupingAfter grouping and aggregation
FiltersIndividual rowsGroups (based on aggregate values)
Can reference aggregatesNoYes
Typical useExcluding rows before they’re groupedExcluding groups whose aggregate fails a condition
PerformanceReduces the row count before the more expensive grouping stepRuns 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 HAVING where WHERE would be cheaper. Filtering rows out early with WHERE avoids aggregating data that will just get discarded by HAVING afterward.
  • Expecting HAVING to filter on a column alias defined in SELECT. Some databases allow referencing a SELECT alias in HAVING; others require repeating the full aggregate expression, since SELECT runs after HAVING in 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.

The Lycoris Team 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.

#Databases #SQL #Backend
The Lycoris Team 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.

#Databases #SQL #Backend