MySQL HAVING Clause
HAVING filters the groups produced by GROUP BY, using aggregate values: customers who ordered more than twice, cities with revenue over ₹2,000, emails that appear more than once. WHERE can't do this, because it runs before groups exist.
1. WHERE vs HAVING
| WHERE | HAVING | |
|---|---|---|
| Filters | Individual rows | Groups |
| Runs | Before GROUP BY | After GROUP BY |
| Can use aggregates (SUM, COUNT…) | No | Yes |
| Can use SELECT aliases | No | Yes (MySQL extension) |
Table: orders (10 rows)
| order_id | customer | city | status | amount | ordered_on |
|---|---|---|---|---|---|
| 1 | Akash | Pune | paid | 1500.00 | 2026-01-03 |
| 2 | Bhumi | Delhi | paid | 650.00 | 2026-01-04 |
| 3 | Akash | Pune | paid | 900.00 | 2026-01-11 |
| 4 | Chetna | Pune | cancelled | 3000.00 | 2026-01-15 |
| 5 | Bhumi | Delhi | paid | 420.00 | 2026-02-01 |
| 6 | Akash | Pune | paid | 300.00 | 2026-02-09 |
| 7 | Dinesh | Kochi | paid | 2600.00 | 2026-02-12 |
| 8 | Bhumi | Delhi | paid | 710.00 | 2026-02-20 |
| 9 | Chetna | Pune | paid | 450.00 | 2026-03-02 |
| 10 | Esha | Kochi | pending | 1200.00 | 2026-03-05 |
2. Filtering Groups
Customers with more than two orders:
SELECT customer, COUNT(*) AS orders
FROM orders
GROUP BY customer
HAVING COUNT(*) > 2;
Result: 2 rows
| customer | orders |
|---|---|
| Akash | 3 |
| Bhumi | 3 |
Trying the same with WHERE fails:
SELECT customer, COUNT(*) FROM orders WHERE COUNT(*) > 2 GROUP BY customer;
ERROR 1111 (HY000): Invalid use of group function
3. Using WHERE and HAVING Together
Count only paid orders (WHERE), then keep cities whose paid revenue exceeds ₹2,000 (HAVING):
SELECT city, COUNT(*) AS paid_orders, SUM(amount) AS revenue
FROM orders
WHERE status = 'paid'
GROUP BY city
HAVING SUM(amount) > 2000
ORDER BY revenue DESC;
Result: 2 rows
| city | paid_orders | revenue |
|---|---|---|
| Pune | 4 | 3150.00 |
| Kochi | 1 | 2600.00 |
Without the WHERE, Chetna's cancelled ₹3,000 order would wrongly count towards Pune.
4. Several Conditions and Aliases
SELECT customer, COUNT(*) AS orders, ROUND(AVG(amount)) AS avg_order
FROM orders
WHERE status = 'paid'
GROUP BY customer
HAVING orders >= 2 AND avg_order < 1000;
Result: 2 rows
| customer | orders | avg_order |
|---|---|---|
| Akash | 3 | 900 |
| Bhumi | 3 | 593 |
HAVING COUNT(*) >= 2 AND AVG(amount) < 1000.5. Finding Duplicates
A classic use: emails that appear more than once, and how many times:
SELECT email, COUNT(*) AS copies, GROUP_CONCAT(id ORDER BY id) AS ids
FROM subscribers
GROUP BY email
HAVING COUNT(*) > 1;
Result: 2 rows
| copies | ids | |
|---|---|---|
| a@x.example | 3 | 1,3,6 |
| b@x.example | 2 | 2,5 |
6. HAVING with MIN, MAX and Dates
Customers whose first order was in January and who are still ordering in February or later:
SELECT customer, MIN(ordered_on) AS first_order, MAX(ordered_on) AS last_order
FROM orders
GROUP BY customer
HAVING MIN(ordered_on) < '2026-02-01' AND MAX(ordered_on) >= '2026-02-01';
Result: 3 rows
| customer | first_order | last_order |
|---|---|---|
| Akash | 2026-01-03 | 2026-02-09 |
| Bhumi | 2026-01-04 | 2026-02-20 |
| Chetna | 2026-01-15 | 2026-03-02 |
7. HAVING Without GROUP BY
Without GROUP BY the whole table is one group. This returns a row only if the condition holds:
SELECT COUNT(*) AS pending FROM orders WHERE status = 'pending' HAVING COUNT(*) > 0;
Result: 1 row
| pending |
|---|
| 1 |
8. Put Row Filters in WHERE
A condition on a plain column (not an aggregate) works in HAVING too, but it is applied after all the grouping work. Put it in WHERE so rows are discarded early, ideally using an index:
SELECT city, SUM(amount) FROM orders GROUP BY city HAVING city <> 'Kochi';
Works, but filters late
| city | SUM(amount) |
|---|---|
| Pune | 6150.00 |
| Delhi | 1780.00 |
SELECT city, SUM(amount) FROM orders WHERE city <> 'Kochi' GROUP BY city;
Better: filters early
| city | SUM(amount) |
|---|---|
| Pune | 6150.00 |
| Delhi | 1780.00 |
9. Practice Exercises
Exercise 1: Months (YYYY-MM) with at least 4 orders of any status.
SELECT DATE_FORMAT(ordered_on, '%Y-%m') AS month, COUNT(*) AS orders
FROM orders
GROUP BY month
HAVING COUNT(*) >= 4;
Result: 2 rows
| month | orders |
|---|---|
| 2026-01 | 4 |
| 2026-02 | 4 |
Exercise 2: Customers whose largest paid order is below ₹1,000.
SELECT customer, MAX(amount) AS biggest
FROM orders WHERE status = 'paid'
GROUP BY customer
HAVING MAX(amount) < 1000;
Result: 2 rows
| customer | biggest |
|---|---|
| Bhumi | 710.00 |
| Chetna | 450.00 |
10. Summary
- HAVING filters groups after GROUP BY; WHERE filters rows before.
- Only HAVING can use aggregates such as COUNT(*) and SUM().
- Combine both: WHERE to choose rows, HAVING to choose groups.
- HAVING COUNT(*) > 1 finds duplicates.
- Keep non-aggregate conditions in WHERE for speed.