The one-line difference
These three clauses trip people up because they all feel like "narrowing down data." But they act at different stages of the query and on different things:
WHEREdecides which rows make it past the door — evaluated before any grouping.HAVINGdecides which groups survive — evaluated afterGROUP BYand aggregation.SELECTdecides which columns (and computed values) you get back.
The reason this ordering matters is SQL's logical execution order. If you want the full picture, see our companion post on SQL execution order (why WHERE runs before SELECT).
SELECT — choosing & computing columns
SELECT is projection: it lists the columns to return and any computed expressions or aggregates. It doesn't filter rows — it shapes the output of the rows that already survived WHERE and HAVING.
-- SELECT picks columns and computes new ones
SELECT
name,
salary,
salary * 12 AS annual_salary
FROM employees;
WHERE — filtering rows (before grouping)
WHERE keeps or drops individual rows based on their own column values. It runs before GROUP BY, so it cannot see aggregates or SELECT aliases.
-- Keep only Engineering employees earning over 80k
SELECT name, department, salary
FROM employees
WHERE department = 'Engineering'
AND salary > 80000;
HAVING — filtering groups (after aggregation)
HAVING filters the groups produced by GROUP BY. This is the only place you can filter on an aggregate like COUNT(), SUM(), or AVG(), because those values only exist after grouping.
-- Only departments with more than 5 people
SELECT department, COUNT(*) AS headcount
FROM employees
GROUP BY department
HAVING COUNT(*) > 5;
HAVING without GROUP BY — it then treats the whole table as one group, e.g. HAVING COUNT(*) > 100. But if your condition isn't an aggregate, use WHERE.WHERE + HAVING together (a real example)
They're not either/or — a well-written aggregate query often uses both. WHERE trims rows first (cheaper), then GROUP BY aggregates, then HAVING filters the resulting groups:
SELECT department, AVG(salary) AS avg_salary
FROM employees
WHERE active = 1 -- 1. row filter (before grouping)
GROUP BY department -- 2. build one group per department
HAVING AVG(salary) > 80000 -- 3. group filter (after aggregation)
ORDER BY avg_salary DESC; -- 4. sort the final result
Read it as: "Among active employees, group by department, then keep only departments whose average salary tops $80k, highest first."
When to use which
- Condition about a single row's raw value (a column) →
WHERE. - Condition about an aggregate of a group (
COUNT,SUM,AVG,MIN,MAX) →HAVING. - Deciding which columns/values to return →
SELECT. - Both a row condition and a group condition? Use both —
WHEREfirst,HAVINGafter.
Side-by-side comparison
| Clause | What it does | Runs (logical order) | Operates on | Aggregates allowed? |
|---|---|---|---|---|
| WHERE | Filters rows | Before GROUP BY | Individual rows | No |
| HAVING | Filters groups | After GROUP BY | Grouped rows | Yes |
| SELECT | Chooses / computes columns | After HAVING | The output projection | Yes (defines them) |
Common mistakes & quick fixes
- Putting an aggregate in
WHERE(WHERE COUNT(*) > 5). Fix: move it toHAVING. - Using
HAVINGfor a plain row condition. It works but filters after grouping — slower. UseWHEREfor column conditions. - Referencing a
SELECTalias inWHERE. Not allowed — repeat the expression or use a subquery/CTE. - Forgetting
GROUP BYwhen selecting a mix of columns and aggregates. Every non-aggregated column inSELECTmust appear inGROUP BY(in standard SQL).
Key takeaways
WHEREfilters rows before grouping;HAVINGfilters groups after aggregation;SELECTpicks the output columns.- Aggregates (
COUNT,SUM,AVG) can be used inHAVINGandSELECT, but never inWHERE. - Use both
WHEREandHAVINGwhen you have a row condition and a group condition. - Filter with
WHEREfirst for efficiency — fewer rows to aggregate. - Column aliases from
SELECTaren't available inWHERE(and, in standard SQL, not inHAVINGeither).