00:00

MySQL HAVING Clause

Last updated: 4 min read

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 filters rows, HAVING filters groupsA pipeline: all rows go through WHERE, which removes individual rows. GROUP BY forms groups and aggregates are computed. HAVING then removes whole groups whose aggregate fails the condition. Finally ORDER BY sorts the result.FROMall rowsWHEREdrop rowsGROUP BYmake groupsHAVINGdrop groupsSELECTcompute columnsORDER BYsorte.g. status = 'paid'e.g. SUM(amount) > 2000WHERE can't see aggregates; HAVING can
Figure 1: HAVING runs after grouping, so it can test aggregates such as COUNT and SUM.
WHEREHAVING
FiltersIndividual rowsGroups
RunsBefore GROUP BYAfter GROUP BY
Can use aggregates (SUM, COUNT…)NoYes
Can use SELECT aliasesNoYes (MySQL extension)

Table: orders (10 rows)

order_idcustomercitystatusamountordered_on
1AkashPunepaid1500.002026-01-03
2BhumiDelhipaid650.002026-01-04
3AkashPunepaid900.002026-01-11
4ChetnaPunecancelled3000.002026-01-15
5BhumiDelhipaid420.002026-02-01
6AkashPunepaid300.002026-02-09
7DineshKochipaid2600.002026-02-12
8BhumiDelhipaid710.002026-02-20
9ChetnaPunepaid450.002026-03-02
10EshaKochipending1200.002026-03-05

2. Filtering Groups

Customers with more than two orders:

query.sql
SELECT customer, COUNT(*) AS orders
FROM orders
GROUP BY customer
HAVING COUNT(*) > 2;

Result: 2 rows

customerorders
Akash3
Bhumi3

Trying the same with WHERE fails:

query.sql
SELECT customer, COUNT(*) FROM orders WHERE COUNT(*) > 2 GROUP BY customer;
Output
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):

query.sql
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

citypaid_ordersrevenue
Pune43150.00
Kochi12600.00

Without the WHERE, Chetna's cancelled ₹3,000 order would wrongly count towards Pune.

4. Several Conditions and Aliases

query.sql
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

customerordersavg_order
Akash3900
Bhumi3593
Using SELECT aliases in HAVING is a MySQL extension. For portable SQL, repeat the expression: HAVING COUNT(*) >= 2 AND AVG(amount) < 1000.

5. Finding Duplicates

A classic use: emails that appear more than once, and how many times:

query.sql
SELECT email, COUNT(*) AS copies, GROUP_CONCAT(id ORDER BY id) AS ids
FROM subscribers
GROUP BY email
HAVING COUNT(*) > 1;

Result: 2 rows

emailcopiesids
a@x.example31,3,6
b@x.example22,5

6. HAVING with MIN, MAX and Dates

Customers whose first order was in January and who are still ordering in February or later:

query.sql
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

customerfirst_orderlast_order
Akash2026-01-032026-02-09
Bhumi2026-01-042026-02-20
Chetna2026-01-152026-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:

query.sql
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:

query.sql
SELECT city, SUM(amount) FROM orders GROUP BY city HAVING city <> 'Kochi';

Works, but filters late

citySUM(amount)
Pune6150.00
Delhi1780.00
query.sql
SELECT city, SUM(amount) FROM orders WHERE city <> 'Kochi' GROUP BY city;

Better: filters early

citySUM(amount)
Pune6150.00
Delhi1780.00

9. Practice Exercises

Exercise 1: Months (YYYY-MM) with at least 4 orders of any status.
solution.sql
SELECT DATE_FORMAT(ordered_on, '%Y-%m') AS month, COUNT(*) AS orders
FROM orders
GROUP BY month
HAVING COUNT(*) >= 4;

Result: 2 rows

monthorders
2026-014
2026-024
Exercise 2: Customers whose largest paid order is below ₹1,000.
solution.sql
SELECT customer, MAX(amount) AS biggest
FROM orders WHERE status = 'paid'
GROUP BY customer
HAVING MAX(amount) < 1000;

Result: 2 rows

customerbiggest
Bhumi710.00
Chetna450.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.