Short answer
WHERE filters individual rows before grouping; HAVING filters groups after GROUP BY and aggregation, so HAVING can reference aggregate functions like COUNT(*) > 5 while WHERE cannot.
Explanation
This follows the logical processing order: FROM/JOIN, then WHERE, then GROUP BY, then HAVING, then SELECT (where aliases are assigned), then ORDER BY, then LIMIT. That order also explains why you usually can't use a SELECT alias in WHERE but can in ORDER BY, and why pushing a filter into WHERE instead of HAVING is faster since it shrinks the set before grouping.
Common mistakes
- Using HAVING for row filters that belong in WHERE.
- Not knowing WHERE runs before GROUP BY.