SQL-Engineering-Handbook

HAVING

Introduction

HAVING is the final filter in the aggregation pipeline — it filters the groups that GROUP BY produced, based on their aggregated values. It’s the single concept in this module most confused with WHERE, and interviewers know it.

Learning Objectives

Concept Overview

WHERE filters individual rows before grouping happens. HAVING filters groups after aggregation has already collapsed rows into summary values.

Clause Operates on Runs
WHERE Individual rows Before GROUP BY
HAVING Aggregated groups After GROUP BY

WHERE filters rows, HAVING filters groups, in the full pipeline

Business Context

“Show me departments with more than 5 employees” cannot be expressed with WHERE, because “more than 5 employees” is a fact about the group, not about any single row — no individual employee row has a “5 employees” value to filter on. That’s exactly the gap HAVING fills.

Syntax

SELECT dept_id, COUNT(*) AS employee_count
FROM employes
GROUP BY dept_id
HAVING COUNT(*) > 1;

Execution Flow

employes rows
   │
   ▼
WHERE      -- (optional) filter rows first, e.g. WHERE dept_id IS NOT NULL
   │
   ▼
GROUP BY dept_id  -- collapse into groups
   │
   ▼
HAVING COUNT(*) > 1   -- keep only groups matching this aggregate condition
   │
   ▼
SELECT dept_id, COUNT(*)   -- final output

Engineering Notes

MySQL Notes

MySQL historically allowed HAVING to reference a SELECT-list alias directly (HAVING employee_count > 1), which is a convenient extension beyond strict ANSI SQL.

PostgreSQL Notes

PostgreSQL also supports referencing a SELECT-list alias inside HAVING, matching MySQL’s behavior here — this is one of the few areas where both engines are more permissive than strict ANSI SQL, in the same direction.

Edge Cases

Common Mistakes

Wrong:

SELECT dept_id, COUNT(*)
FROM employes
GROUP BY dept_id
WHERE COUNT(*) > 1;   -- invalid: aggregate function in WHERE

Correct:

SELECT dept_id, COUNT(*) AS employee_count
FROM employes
GROUP BY dept_id
HAVING COUNT(*) > 1;

Also wrong (using HAVING for a row-level condition that belongs in WHERE, wasting work by grouping unfiltered rows first):

SELECT dept_id, COUNT(*) AS employee_count
FROM employes
GROUP BY dept_id
HAVING dept_id = 10;   -- works, but should be WHERE dept_id = 10

Interview Questions

  1. Why can’t WHERE reference COUNT(*)? What has to happen first?
  2. Rewrite this to use the correct clause: ... GROUP BY dept_id WHERE AVG(salary) > 50000.
  3. Given a query with both WHERE and HAVING, in what order do they logically execute?
  4. Is HAVING with no GROUP BY valid? What does it operate on?

Summary

WHERE filters rows before grouping; HAVING filters groups after aggregation. Any condition involving an aggregate function must go in HAVING. Any condition that can be expressed on raw columns should go in WHERE, for both correctness and performance.

Practice Challenges

  1. Write a query listing cities with more than 2 employees.
  2. Write a query listing departments where the average salary exceeds $50,000, using both WHERE (to exclude unassigned employees first) and HAVING (to filter the resulting averages) in the same query.

Further Reading


Related Topics: GROUP BY · COUNT() · Conditional Aggregation


← Previous Lesson ↑ Module README Next Lesson →