WHERE filters rows; HAVING filters groups
WHERE is row-level — it runs before grouping. HAVING is group-level — it runs after aggregation. The two coexist happily: WHERE narrows the input to GROUP BY; HAVING narrows the output.
The execution pipeline you'll memorise
FROM / JOIN → raw combined rows WHERE → filter individual rows GROUP BY → collapse into groups HAVING → filter groups SELECT → pick columns, compute, alias ORDER BY → sort LIMIT / OFFSET → cap output
This order explains every "why doesn't my query work" question you'll have. Aggregates can't appear in WHERE because aggregates don't exist yet at WHERE time. Aliases defined in SELECT can't be used in WHERE because SELECT runs after WHERE.