Filtering Groups with HAVING
Progress Tracking
Log in to save this lesson and continue from where you left off.
The WHERE Problem
You know WHERE filters rows. But what if you want to filter based on an aggregate result? “Show me departments with more than 3 employees” or “Find customers who’ve spent over $1000 total.”
WHERE can’t do this. It runs before GROUP BY, so the aggregates don’t exist yet:
-- This fails: aggregates aren’t allowed in WHERE
SELECT
department,
COUNT(*) AS emp_count
FROM techcorp_workforce
WHERE COUNT(*) > 3
GROUP BY department;SQL will reject this query. WHERE runs before any grouping happens, so COUNT(*) doesn’t exist yet.
HAVING: The Solution
HAVING is like WHERE, but for aggregated results. It runs after GROUP BY:
-- This works
SELECT
department,
COUNT(*) AS emp_count
FROM techcorp_workforce
GROUP BY department
HAVING COUNT(*) > 3;Now you only see departments with more than 3 employees. The filtering happens after the counting.
WHERE vs HAVING
Here’s the key distinction:
WHEREfilters individual rows before groupingHAVINGfilters groups after aggregation
You can use both in the same query, and often should.
| id | first_name | last_name | department | salary | phone_number | joining_date |
|---|---|---|---|---|---|---|
| 1 | Sarah | Mitchell | HR | 95000 | 555-0101 | 2021-03-15 |
| 2 | Michael | Chen | HR | 88000 | 555-0102 | 2022-06-01 |
| 3 | Emily | Rodriguez | HR | 82500 | 2021-09-20 | |
| 4 | David | Park | HR | 80000 | 555-0104 | 2023-01-10 |
| 5 | Lisa | Thompson | HR | 65000 | 2021-04-05 |
Add a HAVING clause to keep only departments where the average salary exceeds 80000 for recent hires (after 2022). The WHERE already filters to recent hires.
This finds departments where recent hires have an average salary over 80K. WHERE narrows down who’s included, HAVING narrows down which groups you see.
The Execution Order
Understanding when each clause runs helps clarify this:
FROM: Get the tableWHERE: Filter individual rowsGROUP BY: Create groups from remaining rowsHAVING: Filter groupsSELECT: Calculate and choose columnsORDER BY: Sort the results
“What’s the difference between WHERE and HAVING?” Now you know.
The Alias Trap
A common gotcha: in most SQL dialects, you can’t use column aliases in HAVING.
-- This fails in most databases
SELECT
department,
COUNT(*) AS emp_count
FROM techcorp_workforce
GROUP BY department
HAVING emp_count > 3; -- Can’t use alias here!
-- Repeat the aggregate instead
SELECT
department,
COUNT(*) AS emp_count
FROM techcorp_workforce
GROUP BY department
HAVING COUNT(*) > 3; -- Use the actual functionWhy? Because HAVING runs before SELECT, so the alias doesn’t exist yet. You have to repeat the aggregate function.
MySQL allows aliases in HAVING. But for portability, always use the full aggregate expression.
| user_id | timestamp | action |
|---|---|---|
| 0 | 2019-04-25 13:30:15 | page_load |
| 0 | 2019-04-25 13:30:18 | page_load |
| 0 | 2019-04-25 13:30:40 | scroll_down |
| 0 | 2019-04-25 13:30:45 | scroll_up |
| 0 | 2019-04-25 13:31:10 | scroll_down |
Find all actions that occurred more than once using GROUP BY and HAVING.
Common HAVING Patterns
Minimum Threshold
-- Only show categories with significant volume
SELECT
category,
COUNT(*) AS product_count
FROM products
GROUP BY category
HAVING COUNT(*) >= 10;Multiple Conditions
You can combine conditions in HAVING with AND, just like WHERE.
Find departments with more than 2 employees and an average salary over 75000.
Departments With 5 Employees
Use HAVING COUNT(*) to filter groups by member count.
| id | first_name | last_name | age | sex | employee_title | department | salary | target | bonus | city | address | manager_id | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 5 | Max | George | 26 | M | Sales | Sales | 1300 | 200 | 150 | Max@company.com | California | 2638 Richards Avenue | 1 |
| 13 | Katty | Bond | 56 | F | Manager | Management | 150000 | 0 | 300 | Katty@company.com | Arizona | 1 | |
| 11 | Richerd | Gear | 57 | M | Manager | Management | 250000 | 0 | 300 | Richerd@company.com | Alabama | 1 | |
| 10 | Jennifer | Dion | 34 | F | Sales | Sales | 1000 | 200 | 150 | Jennifer@company.com | Alabama | 13 | |
| 19 | George | Joe | 50 | M | Manager | Management | 100000 | 0 | 300 | George@company.com | Florida | 1003 Wyatt Street | 1 |
Find departments with at more than or equal 5 employees.
Key Takeaways
WHEREfilters rows beforeGROUP BYHAVINGfilters groups after aggregationHAVINGcan use aggregate functions;WHEREcannot- You can’t use column aliases in
HAVING(in most dialects) - Use both together:
WHEREfor row-level,HAVINGfor group-level filtering - Remember the execution order:
FROM→WHERE→GROUP BY→HAVING→SELECT→ORDER BY
What’s Next
You’ve seen that aggregates ignore NULLs. But there’s more nuance here than you might expect, and getting it wrong leads to incorrect results. Next, we’ll dig into exactly how NULLs behave in aggregations.