HAVING Clause
Filter grouped results using the HAVING clause.
Introduction to HAVING
You've learned to use WHERE to filter individual rows and GROUP BY to create groups. But what if you want to filter groups based on aggregate calculations?
For example:
- "Show categories with more than 5 products"
- "Show departments with average salary > $80,000"
- "Show customers who placed at least 3 orders"
You can't use WHERE for this because WHERE filters rows before grouping happens. You need HAVING to filter groups after aggregation!
Filters groups based on aggregate conditions. Works like WHERE, but operates on groups after GROUP BY, not on individual rows before grouping.
Aspect | WHERE | HAVING |
|---|---|---|
| Filters | Individual rows | Groups |
| When | Before GROUP BY | After GROUP BY |
| Can use aggregates? | NO ❌ | YES ✓ |
| Example | ` WHERE price > 100 ` | ` HAVING COUNT(*) > 5 ` |
Basic Syntax:
SELECT column, aggregate_function(column)
FROM table_name
GROUP BY column
HAVING aggregate_condition;
Let's work with our familiar tables:
id | product_name | category | price | stock |
|---|---|---|---|---|
| 1 | Laptop | Electronics | 999.99 | 45 |
| 2 | Mouse | Electronics | 29.99 | 120 |
| 3 | Desk Chair | Furniture | 199.99 | 30 |
| 4 | Monitor | Electronics | 349.99 | 60 |
| 5 | Desk Lamp | Furniture | 39.99 | 85 |
| 6 | Keyboard | Electronics | 79.99 | 95 |
| 7 | Bookshelf | Furniture | 159.99 | 20 |
| 8 | Office Chair | Furniture | 249.99 | 15 |
Basic HAVING Examples
Problem: Show categories that have more than 3 products.
-- Categories with more than 3 products
SELECT category, COUNT(*) AS product_count
FROM products
GROUP BY category
HAVING COUNT(*) > 3;What happened:
- GROUP BY created two groups: Electronics (4 products) and Furniture (4 products)
- HAVING filtered to only groups where COUNT(*) > 3
- Both categories have exactly 4 products, but let's change the condition...
-- Categories with at least 4 products
SELECT category, COUNT(*) AS product_count
FROM products
GROUP BY category
HAVING COUNT(*) >= 4;Now both categories pass the filter!
Another example - average price:
-- Categories with average price > $150
SELECT
category,
COUNT(*) AS products,
ROUND(AVG(price), 2) AS avg_price
FROM products
GROUP BY category
HAVING AVG(price) > 150;Electronics average is 364.99 (passes the filter). Furniture average is 162.49 (doesn't pass, so it's excluded).
WHERE vs HAVING - Understanding the Difference
Let's see the critical difference with a side-by-side comparison:
-- WRONG: Can't use aggregate in WHERE
SELECT category, COUNT(*) AS product_count
FROM products
WHERE COUNT(*) > 3 -- ❌ ERROR!
GROUP BY category;-- CORRECT: Use HAVING for aggregate conditions
SELECT category, COUNT(*) AS product_count
FROM products
GROUP BY category
HAVING COUNT(*) > 3; -- ✓ WORKS!WHERE filters individual rows:
-- WHERE: Filter rows where price > 100 (BEFORE grouping)
SELECT category, COUNT(*) AS product_count
FROM products
WHERE price > 100
GROUP BY category;WHERE excluded Mouse (29.99) and Keyboard (79.99) and Desk Lamp (39.99) before grouping.
HAVING filters groups:
-- HAVING: Filter groups with count > 2 (AFTER grouping)
SELECT category, COUNT(*) AS product_count
FROM products
GROUP BY category
HAVING COUNT(*) > 2;HAVING excluded groups with 2 or fewer products (there are none in this case).
Combining WHERE and HAVING
You can use both WHERE and HAVING in the same query! WHERE filters rows first, then GROUP BY groups them, then HAVING filters the groups.
Execution order:
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY
-- Categories with >2 expensive products (price > 100)
SELECT category, COUNT(*) AS expensive_products
FROM products
WHERE price > 100 -- Filter: only expensive products
GROUP BY category -- Group by category
HAVING COUNT(*) > 2; -- Filter: categories with >2 productsStep-by-step:
- WHERE filters to products with price > 100: Laptop, Monitor, Desk Chair, Bookshelf, Office Chair
- GROUP BY groups these 5 products by category
- Electronics: 2 products (Laptop, Monitor)
- Furniture: 3 products (Desk Chair, Bookshelf, Office Chair)
- HAVING filters to groups with COUNT(*) > 2
- Electronics (2) is excluded
- Furniture (3) passes the filter
Another example:
id | name | department | salary | years_experience |
|---|---|---|---|---|
| 1 | Alice | Engineering | 95000 | 5 |
| 2 | Bob | Engineering | 85000 | 3 |
| 3 | Charlie | Engineering | 75000 | 2 |
| 4 | Diana | Sales | 70000 | 4 |
| 5 | Eve | Sales | 75000 | 6 |
| 6 | Frank | Sales | 65000 | 1 |
| 7 | Grace | Marketing | 80000 | 7 |
-- Departments with 2+ experienced employees (3+ years) and avg salary > 70k
SELECT
department,
COUNT(*) AS experienced_employees,
ROUND(AVG(salary), 2) AS avg_salary
FROM employees
WHERE years_experience >= 3 -- Only experienced employees
GROUP BY department
HAVING COUNT(*) >= 2 -- At least 2 employees
AND AVG(salary) > 70000; -- Average salary > 70kWHERE filtered to 4 employees (Alice, Bob, Diana, Eve, Grace). Then grouped by department and filtered groups with HAVING.
HAVING with Multiple Conditions
Just like WHERE, you can use AND, OR, and multiple conditions in HAVING:
-- Categories with 3+ products AND average price between 100-400
SELECT
category,
COUNT(*) AS product_count,
ROUND(AVG(price), 2) AS avg_price,
SUM(stock) AS total_stock
FROM products
GROUP BY category
HAVING COUNT(*) >= 3
AND AVG(price) BETWEEN 100 AND 400;-- Categories with low count OR low total stock
SELECT
category,
COUNT(*) AS products,
SUM(stock) AS total_stock
FROM products
GROUP BY category
HAVING COUNT(*) < 3
OR SUM(stock) < 200;Furniture has 4 products (doesn't meet COUNT(*) < 3) but total_stock is 150 (meets SUM(stock) < 200), so it passes the OR condition.
HAVING with ORDER BY
You can sort filtered groups with ORDER BY (which comes after HAVING):
-- Departments with 2+ employees, sorted by average salary
SELECT
department,
COUNT(*) AS employees,
ROUND(AVG(salary), 2) AS avg_salary
FROM employees
GROUP BY department
HAVING COUNT(*) >= 2
ORDER BY avg_salary DESC;Full query order:
SELECT ...
FROM ...
WHERE ...
GROUP BY ...
HAVING ...
ORDER BY ...
LIMIT ...
Using Column Aliases in HAVING
In some databases (like MySQL), you can use column aliases in HAVING:
-- Using alias in HAVING (works in MySQL, not all databases)
SELECT
category,
COUNT(*) AS product_count,
ROUND(AVG(price), 2) AS avg_price
FROM products
GROUP BY category
HAVING product_count > 3
AND avg_price > 150;Database compatibility: Some databases (PostgreSQL, Oracle) don't support aliases in HAVING. For maximum compatibility, use the full expression:
HAVING COUNT(*) > 3 AND AVG(price) > 150
Instead of:
HAVING product_count > 3 AND avg_price > 150
Practical Examples
Example 1: Find popular products
order_id | product_id | quantity |
|---|---|---|
| 1 | 101 | 2 |
| 2 | 102 | 1 |
| 3 | 101 | 3 |
| 4 | 103 | 1 |
| 5 | 101 | 1 |
| 6 | 102 | 2 |
| 7 | 104 | 5 |
-- Products ordered more than 2 times
SELECT
product_id,
COUNT(*) AS times_ordered,
SUM(quantity) AS total_quantity
FROM order_items
GROUP BY product_id
HAVING COUNT(*) > 2
ORDER BY times_ordered DESC;Example 2: Active customers
id | customer_id | order_date | total_amount |
|---|---|---|---|
| 1 | 501 | 2024-01-15 | 150.00 |
| 2 | 502 | 2024-01-20 | 200.00 |
| 3 | 501 | 2024-02-01 | 75.00 |
| 4 | 503 | 2024-02-05 | 300.00 |
| 5 | 501 | 2024-02-10 | 125.00 |
-- Customers with 2+ orders and total spending > $200
SELECT
customer_id,
COUNT(*) AS order_count,
SUM(total_amount) AS total_spent,
ROUND(AVG(total_amount), 2) AS avg_order
FROM orders
GROUP BY customer_id
HAVING COUNT(*) >= 2
AND SUM(total_amount) > 200
ORDER BY total_spent DESC;Example 3: Department performance
-- Large departments (5+ employees) with high avg salary (>75k)
SELECT
department,
COUNT(*) AS headcount,
ROUND(AVG(salary), 2) AS avg_salary,
MIN(salary) AS min_salary,
MAX(salary) AS max_salary
FROM employees
WHERE salary >= 60000 -- Only consider employees earning 60k+
GROUP BY department
HAVING COUNT(*) >= 2
AND AVG(salary) > 75000
ORDER BY avg_salary DESC;Common Mistakes to Avoid
Don't make these mistakes:
-
Using aggregates in WHERE: ❌
- Wrong:
WHERE COUNT(*) > 5 - Right:
HAVING COUNT(*) > 5 - WHERE can't use aggregates!
- Wrong:
-
Using HAVING without GROUP BY: Usually wrong ❌
SELECT * FROM products HAVING price > 100- Should be:
WHERE price > 100 - HAVING is for filtering groups, not individual rows
-
Filtering non-aggregates with HAVING: Works but confusing ❌
GROUP BY category HAVING category = 'Electronics'- Should be:
WHERE category = 'Electronics' ... GROUP BY category - Use WHERE for row filters, HAVING for group filters
-
Forgetting GROUP BY: ❌
SELECT category, COUNT(*) FROM products HAVING COUNT(*) > 3- Need GROUP BY before HAVING!
-
Wrong execution order assumption: ❌
- You can't reference a WHERE-filtered column in HAVING if it's been excluded
- Understand: FROM → WHERE → GROUP BY → HAVING → SELECT
-
Using SELECT aliases in HAVING (on some databases): ❌
- Works in MySQL, fails in PostgreSQL
- For portability, use full expression:
HAVING AVG(price) > 100
-
Confusing HAVING and WHERE: ❌
- WHERE: "Filter rows before grouping"
- HAVING: "Filter groups after aggregation"
- Not interchangeable!
Decision Guide: WHERE vs HAVING
Question | Use | Example |
|---|---|---|
| Filter individual rows? | WHERE | `WHERE price > 100` |
| Filter based on aggregates? | HAVING | `HAVING COUNT(*) > 5` |
| Filter before grouping? | WHERE | `WHERE status = 'active'` |
| Filter after grouping? | HAVING | `HAVING AVG(salary) > 75000` |
| No GROUP BY in query? | WHERE | `WHERE category = 'Electronics'` |
| Using COUNT, SUM, AVG, etc? | HAVING | `HAVING SUM(revenue) > 10000` |
Key Takeaways
What you learned:
✅ HAVING filters groups after GROUP BY and aggregation ✅ WHERE filters individual rows before grouping ✅ HAVING can use aggregate functions (COUNT, SUM, AVG, MIN, MAX) ✅ WHERE cannot use aggregate functions ✅ You can use both WHERE and HAVING in the same query ✅ Execution order: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY ✅ HAVING supports multiple conditions with AND/OR ✅ Use WHERE for row filters, HAVING for group filters
Coming up next: We'll learn about JOINs to combine data from multiple tables!
Practice Exercise: Try these queries:
- Find categories with more than 5 products
- Find departments with average salary above $80,000
- Find customers who placed 3+ orders with total spending > $500
- Find products (grouped by category) where the most expensive item costs > $500
- Combine: Filter to active employees (WHERE), group by department, show departments with 3+ employees and avg salary > $75k (HAVING)