GROUP BY collapses rows that share a value in the specified column(s) into a single group, and aggregate functions (COUNT, SUM, AVG, MIN, MAX) then compute one summary value per group. WHERE filters individual rows before grouping ever happens, so it cannot reference an aggregate result; HAVING filters entire groups after aggregation, which is the only place a condition on an aggregate value (like COUNT(*) > 5) can go.
GROUP BY: one row per group
Every column in SELECT that isn't inside an aggregate function must appear in GROUP BY — the engine needs to know how to collapse the other rows within each group.
SELECT category, COUNT(*) AS product_count, AVG(price) AS avg_price
FROM products
GROUP BY category;
-- one row per DISTINCT category, with aggregates computed within eachHAVING: filtering groups, not rows
This is the one thing WHERE fundamentally cannot do — filter on the RESULT of an aggregate, since WHERE runs before aggregation happens.
SELECT category, COUNT(*) AS product_count
FROM products
GROUP BY category
HAVING COUNT(*) > 10;
-- WHERE COUNT(*) > 10 here would be a SYNTAX ERROR — COUNT isn't computed yet at WHERE's stageWHERE and HAVING together
These commonly appear in the same query: WHERE narrows down which rows even enter the grouping step, and HAVING then filters the resulting groups — the two operate at genuinely different stages of query execution.
SELECT category, COUNT(*) AS product_count
FROM products
WHERE in_stock = true -- filters ROWS first
GROUP BY category
HAVING COUNT(*) > 10; -- filters GROUPS after aggregation