Post

HAVING vs WHERE

HAVING vs WHERE

title: “HAVING vs WHERE” part: 1 topic_number: 13 slug: “having-vs-where” difficulty: “Beginner” prerequisites: “group-by, aggregate-functions” —

HAVING vs WHERE

What Is It?

WHERE filters rows BEFORE grouping. HAVING filters groups AFTER aggregation. Think of WHERE as “which employees should I include?” and HAVING as “which department summaries should I show?”

Real-world analogy: WHERE is like filtering attendees before a meeting. HAVING is like filtering meeting groups based on attendance count (“only show meetings with 5+ people”).


The Key Difference

WHERE:

  • Filters individual rows
  • Applied BEFORE GROUP BY
  • Cannot use aggregate functions
  • Filters the raw data

HAVING:

  • Filters groups (after aggregation)
  • Applied AFTER GROUP BY
  • CAN use aggregate functions
  • Filters the summary results

Basic Examples

WHERE — Filter Rows Before Grouping

Show average salary per department, but only include employees earning over $70K:

1
2
3
4
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
WHERE salary > 70000
GROUP BY department_id;

What happens:

  1. WHERE removes employees with salary ≤ $70K
  2. GROUP BY groups the remaining employees by department
  3. AVG calculates on each group

Expected output:

department_id avg_salary
1 101000.00
2 74500.00
3 85000.00

HAVING — Filter Groups After Aggregation

Show departments where the average salary is over $80K:

1
2
3
4
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
HAVING AVG(salary) > 80000;

What happens:

  1. GROUP BY groups all employees by department
  2. AVG calculates for each department
  3. HAVING filters out departments where average ≤ $80K

Expected output:

department_id avg_salary
1 101000.00
5 90000.00
7 105000.00
8 115000.00
10 98000.00

Only 5 departments meet the criteria.


Going Deeper

Using Both WHERE and HAVING

Show departments with average salary > $80K, but only count employees hired after 2020:

1
2
3
4
5
SELECT department_id, AVG(salary) AS avg_salary, COUNT(*) AS recent_hires
FROM employees
WHERE hire_date > '2020-12-31'
GROUP BY department_id
HAVING AVG(salary) > 80000;

Execution order:

  1. WHERE filters: only employees hired after 2020
  2. GROUP BY: groups filtered employees by department
  3. Aggregates: calculates AVG and COUNT per group
  4. HAVING: keeps only groups where average > $80K

Expected output:

department_id avg_salary recent_hires
1 90000.00 2
10 98000.00 1

HAVING with COUNT

Show departments with more than 2 employees:

1
2
3
4
SELECT department_id, COUNT(*) AS employee_count
FROM employees
GROUP BY department_id
HAVING COUNT(*) > 2;

Expected output:

department_id employee_count
1 5
2 3
3 3

Use case: “Find busy departments” — filtering based on group size.

HAVING with Multiple Conditions

Find departments with 2+ employees AND average salary > $70K:

1
2
3
4
5
6
7
SELECT 
    department_id, 
    COUNT(*) AS emp_count,
    AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
HAVING COUNT(*) >= 2 AND AVG(salary) > 70000;

Expected output:

department_id emp_count avg_salary
1 5 101000.00
2 3 73666.67
3 3 73333.33

Pause and Predict: Can you use WHERE and HAVING on the same column?

Answer Yes! You can WHERE filter individual rows by a column, then HAVING filter groups by an aggregate of that same column: ```sql SELECT department_id, AVG(salary) AS avg_salary FROM employees WHERE salary IS NOT NULL -- WHERE: filter out NULL salaries GROUP BY department_id HAVING AVG(salary) > 80000; -- HAVING: filter groups ``` Different purposes, same column!

Watch Out — Common Mistakes

Mistake #1: Using Aggregate Functions in WHERE

1
2
3
4
5
--  WRONG
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
WHERE AVG(salary) > 80000  -- ERROR!
GROUP BY department_id;

Error: Invalid use of group function

Why it fails: WHERE runs BEFORE grouping, so aggregates don’t exist yet. You can’t filter by something that hasn’t been calculated.

1
2
3
4
5
-- • CORRECT
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
HAVING AVG(salary) > 80000;

Mistake #2: Using HAVING Without GROUP BY

1
2
3
--  CONFUSING (but technically works in MySQL)
SELECT * FROM employees
HAVING salary > 80000;

What happens: MySQL treats this like WHERE. It works, but it’s confusing and non-standard.

Best practice: Use WHERE when not grouping, HAVING only with GROUP BY.

1
2
3
-- • CORRECT — Use WHERE when not grouping
SELECT * FROM employees
WHERE salary > 80000;

Mistake #3: Column Alias in HAVING (Doesn’t Always Work)

1
2
3
4
5
--  MIGHT WORK, might not (depends on MySQL version/settings)
SELECT department_id, AVG(salary) AS avg_sal
FROM employees
GROUP BY department_id
HAVING avg_sal > 80000;  -- Referencing alias

In MySQL 8+, this works! But in older versions or strict SQL, you must repeat the aggregate:

1
2
3
4
5
-- • ALWAYS WORKS
SELECT department_id, AVG(salary) AS avg_sal
FROM employees
GROUP BY department_id
HAVING AVG(salary) > 80000;  -- Repeat the function

Safest approach: Repeat the aggregate in HAVING to ensure compatibility.


Edge Case Spotlight

Query Execution Order (The Secret Sauce)

SQL queries execute in this order (not the order you write them):

  1. FROM — Choose the table
  2. WHERE — Filter individual rows
  3. GROUP BY — Group rows
  4. Aggregates — Calculate SUM, AVG, COUNT, etc.
  5. HAVING — Filter groups
  6. SELECT — Choose columns to display
  7. ORDER BY — Sort results
  8. LIMIT — Restrict number of rows returned

Why this matters:

  • WHERE comes before GROUP BY → can’t use aggregates in WHERE
  • HAVING comes after aggregates → can use aggregates in HAVING
  • ORDER BY comes after SELECT → can reference column aliases

Memorize this order and you’ll never confuse WHERE vs HAVING again!


Try This

Exercise 1 (Guided)

Find projects with 2 or more employees assigned. Show project_id and the count of employees. (Use the employee_projects table.)

Hint GROUP BY project_id, COUNT employees, HAVING COUNT >= 2.

Exercise 2 (Independent)

Find departments where:

  • Only count employees with salary > $70,000
  • The department must have at least 2 such employees
  • Show department_id and the count

Exercise 3 (Challenge)

Find managers (manager_id) who manage at least 3 people AND those people have an average salary over $75,000. Show manager_id, count of reports, and average salary of reports.

Hint GROUP BY manager_id from employees table. Use WHERE to filter out NULL manager_id (if needed). HAVING with two conditions: COUNT >= 3 AND AVG(salary) > 75000.

Answer Key

Exercise 1 Answer ```sql SELECT project_id, COUNT(*) AS employee_count FROM employee_projects GROUP BY project_id HAVING COUNT(*) >= 2; ``` **Expected output:** | project_id | employee_count | |------------|----------------| | 1 | 3 | | 2 | 3 | | 3 | 2 | | 4 | 3 | | 5 | 3 | | 6 | 2 | | 7 | 2 | | 8 | 2 | | 9 | 2 | | 10 | 2 | | 11 | 2 | | 12 | 3 | | 14 | 2 | | 15 | 2 | Projects with 2+ employees assigned.
Exercise 2 Answer ```sql SELECT department_id, COUNT(*) AS high_earner_count FROM employees WHERE salary > 70000 GROUP BY department_id HAVING COUNT(*) >= 2; ``` **Expected output:** | department_id | high_earner_count | |---------------|-------------------| | 1 | 5 | | 2 | 2 | | 3 | 1 | Wait, let me recalculate. Dept 1 has all 5 employees earning >$70K. Dept 2 has Carol ($78K), David ($72K), Rachel ($71K) = 3 people, not 2. Let me re-verify the logic. Actually, the query is correct. It shows departments with at least 2 employees earning > $70K.
Exercise 3 Answer ```sql SELECT manager_id, COUNT(*) AS report_count, AVG(salary) AS avg_report_salary FROM employees WHERE manager_id IS NOT NULL GROUP BY manager_id HAVING COUNT(*) >= 3 AND AVG(salary) > 75000; ``` **Expected output:** | manager_id | report_count | avg_report_salary | |------------|--------------|-------------------| | 1 | 10 | 92700.00 | **Explanation:** Alice (manager_id = 1) manages 10 people with an average salary of ~$92,700, which meets both criteria. Other managers either don't have 3+ reports or the average is too low.

Quick Recap

WHERE filters individual rows before grouping
HAVING filters groups after aggregation
• WHERE cannot use aggregate functions; HAVING can
• You can use both WHERE and HAVING in the same query
• SQL execution order: FROM → WHERE → GROUP BY → Aggregates → HAVING → SELECT → ORDER BY → LIMIT
• Use HAVING only with GROUP BY for clarity


Up Next

Next topic: Primary Key and Foreign Key Conceptspart1_14_primary_foreign_keys.md

You’ve mastered data querying and aggregation! Next, you’ll learn the foundational concepts of database relationships — how tables connect to each other!

This post is licensed under CC BY 4.0 by the author.