Filtering Groups with HAVING
Now that you've mastered GROUP BY, it's time to control which groups make it into your results. The marketplace schema makes this easy to visualize: maybe you want to see only the categories that have at least four products, or order statuses where the average order total is above 50. The HAVING clause lets you filter groups after aggregation, complementing the WHERE clause that filters individual rows before grouping.
Learning Goals
- Know when to use
WHEREvsHAVING - Filter grouped data using counts, averages, and sums
- Combine row-level and group-level filters in the same query
- Avoid common mistakes that lead to confusing results
Understanding HAVING
HAVING evaluates conditions on aggregated groups. A typical pattern looks like this:
SELECT cat.category_name,
AVG(p.price) AS avg_price
FROM product AS p
JOIN category AS cat ON cat.category_id = p.category_id
GROUP BY cat.category_name
HAVING AVG(p.price) >= 40;
This query shows only categories whose products average 40 or more.
WHERE vs HAVING
Remember the execution order: WHERE filters rows -> GROUP BY creates groups -> HAVING filters groups -> ORDER BY sorts the final output.
-- Products priced above 30, grouped by category
SELECT cat.category_name,
COUNT(*) AS product_count
FROM product AS p
JOIN category AS cat ON cat.category_id = p.category_id
WHERE p.price > 30
GROUP BY cat.category_name;
-- Categories that have more than 3 products above 30
SELECT cat.category_name,
COUNT(*) AS product_count
FROM product AS p
JOIN category AS cat ON cat.category_id = p.category_id
WHERE p.price > 30
GROUP BY cat.category_name
HAVING COUNT(*) > 3;
Practical HAVING Examples
Filter by number of rows in each group
SELECT cat.category_name,
COUNT(*) AS total_products
FROM product AS p
JOIN category AS cat ON cat.category_id = p.category_id
GROUP BY cat.category_name
HAVING COUNT(*) >= 4;
Filter by aggregate averages
SELECT o.status,
AVG(o.order_total) AS avg_order_total
FROM orders AS o
GROUP BY o.status
HAVING AVG(o.order_total) >= 50;
Combine multiple aggregate conditions
SELECT cat.category_name,
o.status,
COUNT(*) AS items_sold,
SUM(oi.quantity * oi.unit_price) AS revenue
FROM order_item AS oi
JOIN orders AS o ON o.order_id = oi.order_id
JOIN product AS p ON p.product_id = oi.product_id
JOIN category AS cat ON cat.category_id = p.category_id
GROUP BY cat.category_name, o.status
HAVING COUNT(*) >= 3
AND SUM(oi.quantity * oi.unit_price) >= 100;
Combining WHERE and HAVING
Strong queries filter out unnecessary rows early with WHERE, then use HAVING for final group-level checks.
SELECT cat.category_name,
COUNT(*) AS delivered_items,
COUNT(DISTINCT o.customer_id) AS unique_customers
FROM order_item AS oi
JOIN orders AS o ON o.order_id = oi.order_id
JOIN product AS p ON p.product_id = oi.product_id
JOIN category AS cat ON cat.category_id = p.category_id
WHERE o.order_date LIKE '2025-03%'
AND o.status = 'Delivered'
GROUP BY cat.category_name
HAVING COUNT(*) >= 2;
Common Pitfalls
- Aggregates in WHERE: Conditions like
WHERE COUNT(*) > 3fail becauseWHEREruns before grouping. - Missing GROUP BY:
HAVINGexpects grouped data. WithoutGROUP BY, the entire dataset is treated as one group. - Order matters: The correct order is
WHERE,GROUP BY,HAVING, thenORDER BY. - Too few row filters: Use
WHEREto shrink the data before grouping; it keepsHAVINGefficient. - Alias confusion: Some engines don't let you reference
SELECTaliases insideHAVING; repeat the expression if needed.
Summary
- Use
WHEREfor individual rows,HAVINGfor aggregated groups. - Combine both clauses to answer nuanced questions about the marketplace data.
HAVINGshines when you need thresholds on counts, averages, or sums after grouping.
Quiz
Show quiz
-
Which clause filters rows before grouping occurs?
A)GROUP BY
B)WHERE
C)HAVING
D)ORDER BY -
How would you list categories that have at least three products priced above 30?
A)WHERE COUNT(*) >= 3
B)HAVING COUNT(*) >= 3
C)HAVING price >= 30
D)WHERE price >= 30 -
What does
HAVING AVG(o.order_total) > 50evaluate?
A) Each individual order row
B) Each group of rows created byGROUP BY
C) Only orders with totals greater than 50
D) It causes an error becauseAVGmust be inSELECT -
True or False: You can use both
WHEREandHAVINGin the same query.
A) True
B) False
Answers
- B
- B
- B
- A