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:

  • WHERE decides which rows make it past the door — evaluated before any grouping.
  • HAVING decides which groups survive — evaluated after GROUP BY and aggregation.
  • SELECT decides 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.sql
-- 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.

where.sql
-- 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.

having.sql
-- Only departments with more than 5 people
SELECT department, COUNT(*) AS headcount
FROM employees
GROUP BY department
HAVING COUNT(*) > 5;
Tip: you can use 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:

where-and-having.sql
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 bothWHERE first, HAVING after.

Side-by-side comparison

ClauseWhat it doesRuns (logical order)Operates onAggregates allowed?
WHEREFilters rowsBefore GROUP BYIndividual rowsNo
HAVINGFilters groupsAfter GROUP BYGrouped rowsYes
SELECTChooses / computes columnsAfter HAVINGThe output projectionYes (defines them)

Common mistakes & quick fixes

  • Putting an aggregate in WHERE (WHERE COUNT(*) > 5). Fix: move it to HAVING.
  • Using HAVING for a plain row condition. It works but filters after grouping — slower. Use WHERE for column conditions.
  • Referencing a SELECT alias in WHERE. Not allowed — repeat the expression or use a subquery/CTE.
  • Forgetting GROUP BY when selecting a mix of columns and aggregates. Every non-aggregated column in SELECT must appear in GROUP BY (in standard SQL).

Key takeaways

  • WHERE filters rows before grouping; HAVING filters groups after aggregation; SELECT picks the output columns.
  • Aggregates (COUNT, SUM, AVG) can be used in HAVING and SELECT, but never in WHERE.
  • Use both WHERE and HAVING when you have a row condition and a group condition.
  • Filter with WHERE first for efficiency — fewer rows to aggregate.
  • Column aliases from SELECT aren't available in WHERE (and, in standard SQL, not in HAVING either).