The SQL HAVING clause filters grouped or aggregate results. Use it when the condition depends on values such as COUNT(), SUM(), AVG(), MIN() or MAX().
SELECT category,
type,
COUNT(*) AS total
FROM documents
GROUP BY category, type
HAVING COUNT(*) < 3;
This groups the documents table by category and type, then keeps only groups containing fewer than three rows.
WHERE filters individual rows before grouping. HAVING filters grouped aggregate results after grouping.SELECT group_column,
aggregate_function(column_name) AS aggregate_alias
FROM table_name
WHERE row_condition
GROUP BY group_column
HAVING aggregate_condition
ORDER BY group_column;
The normal logical flow is: filter source rows with WHERE, form groups with GROUP BY, calculate aggregates, filter those groups with HAVING, and finally sort the result with ORDER BY.
The original Plus2net example uses this documents table:
| d_id | name | type | category | price | stock |
|---|---|---|---|---|---|
| 1 | Book1 | Book | Management | 220 | 10 |
| 2 | Book2 | CD | Management | 120 | 8 |
| 3 | Book3 | Report | Management | 25 | 4 |
| 4 | Book4 | Book | Management | 55 | 8 |
| 5 | Document1 | Book | Management | 15 | 23 |
| 6 | Document2 | CD | Computers | 80 | 45 |
| 7 | Document3 | Report | Computers | 55 | 65 |
| 8 | Book5 | Report | Management | 80 | 10 |
| 9 | Document4 | CD | Management | 72 | 5 |
| 10 | Book 8 | Book | Computers | 88 | 6 |
| 11 | Book 9 | CD | Computers | 100 | 5 |
| 12 | Document5 | Report | Computers | 85 | 8 |
| 13 | Book 10 | Book | Computers | 150 | 5 |
First group the records by category and type:
SELECT category,
type,
COUNT(*) AS total
FROM documents
GROUP BY category, type
ORDER BY category, type;
| category | type | total |
|---|---|---|
| Computers | Book | 2 |
| Computers | CD | 2 |
| Computers | Report | 2 |
| Management | Book | 3 |
| Management | CD | 2 |
| Management | Report | 2 |
Now keep only groups with fewer than three rows:
SELECT category,
type,
COUNT(*) AS total
FROM documents
GROUP BY category, type
HAVING COUNT(*) < 3
ORDER BY category, type;
| category | type | total |
|---|---|---|
| Computers | Book | 2 |
| Computers | CD | 2 |
| Computers | Report | 2 |
| Management | CD | 2 |
| Management | Report | 2 |
The Management / Book group is excluded because its count is 3.
The older version of this page showed an invalid query by putting WHERE after GROUP BY. The deeper issue is not only clause order: WHERE cannot filter a result based on an aggregate that has not yet been calculated.
This is wrong:
-- Wrong: aggregate filtering belongs in HAVING
SELECT category,
type,
COUNT(*) AS total
FROM documents
WHERE COUNT(*) < 3
GROUP BY category, type;
Use HAVING for the aggregate condition:
SELECT category,
type,
COUNT(*) AS total
FROM documents
GROUP BY category, type
HAVING COUNT(*) < 3;
| Clause | What it filters | When it acts |
|---|---|---|
WHERE | Individual source rows | Before GROUP BY and aggregate calculation |
HAVING | Grouped / aggregate results | After GROUP BY and aggregate calculation |
Use both clauses when you need to filter source rows and then filter the groups created from those rows.
For example, first consider only documents priced at 50 or more, then keep groups containing at least two of those records:
SELECT category,
type,
COUNT(*) AS total
FROM documents
WHERE price >= 50
GROUP BY category, type
HAVING COUNT(*) >= 2
ORDER BY category, type;
The WHERE condition removes low-price rows before grouping. HAVING then checks the size of each remaining group.
MySQL allows a SELECT alias to be referenced in HAVING:
SELECT category,
type,
COUNT(*) AS total
FROM documents
GROUP BY category, type
HAVING total < 3;
The equivalent aggregate expression is:
HAVING COUNT(*) < 3
Return category/type groups whose average price is above 80:
SELECT category,
type,
AVG(price) AS average_price
FROM documents
GROUP BY category, type
HAVING AVG(price) > 80
ORDER BY average_price DESC;
See SQL AVG() for average calculations.
Return categories whose total stock is greater than 50:
SELECT category,
SUM(stock) AS total_stock
FROM documents
GROUP BY category
HAVING SUM(stock) > 50;
See SQL SUM().
Return categories where the highest price is at least 150:
SELECT category,
MAX(price) AS highest_price
FROM documents
GROUP BY category
HAVING MAX(price) >= 150;
Or keep groups whose minimum stock is below 6:
SELECT category,
type,
MIN(stock) AS minimum_stock
FROM documents
GROUP BY category, type
HAVING MIN(stock) < 6;
Related tutorials: MAX() and MIN().
Aggregate conditions can be combined with AND or OR.
SELECT category,
type,
COUNT(*) AS total,
AVG(price) AS average_price
FROM documents
GROUP BY category, type
HAVING COUNT(*) >= 2
AND AVG(price) > 70;
Both aggregate conditions must be true for the group to remain.
A common use of HAVING is identifying values that appear more than once:
SELECT name,
COUNT(*) AS duplicate_count
FROM student2_1
GROUP BY name
HAVING COUNT(*) > 1;
GROUP BY forms one group per name, and HAVING removes groups that occur only once.
HAVING is most commonly used with GROUP BY, but MySQL also permits HAVING in some queries without an explicit GROUP BY.
For example, the complete filtered result can act as one aggregate group:
SELECT COUNT(*) AS total_documents
FROM documents
HAVING COUNT(*) > 10;
If the aggregate condition is true, the aggregate row is returned; otherwise no row is returned.
When the HAVING threshold comes from application input, bind it as a prepared-statement value.
<?php
require 'config.php';
$minimum_count=2;
$stmt=$dbo->prepare(
"SELECT category,
type,
COUNT(*) AS total
FROM documents
GROUP BY category, type
HAVING COUNT(*) >= :minimum_count
ORDER BY category, type"
);
$stmt->bindValue(
':minimum_count',
$minimum_count,
PDO::PARAM_INT
);
$stmt->execute();
while($row=$stmt->fetch(PDO::FETCH_ASSOC)){
$category=htmlspecialchars(
(string)$row['category'],
ENT_QUOTES,
'Windows-1252'
);
$type=htmlspecialchars(
(string)$row['type'],
ENT_QUOTES,
'Windows-1252'
);
$total=(int)$row['total'];
echo "$category - $type : $total<br>";
}
The HAVING threshold is data, so it can be bound safely. SQL identifiers such as column names should remain application-controlled.
EXPLAIN when a grouped production query is unexpectedly slow.Aggregate results such as COUNT(), SUM() and AVG() are calculated after WHERE. Use HAVING for conditions on those results.
If a condition applies to individual source rows, WHERE is normally the correct place and can reduce the amount of data that must be grouped.
The SQL clause order is WHERE before GROUP BY, then HAVING after GROUP BY.
Aliases such as total are names created for the query result. MySQL can use SELECT aliases in HAVING, but portability differs across database systems.
Without GROUP BY, an aggregate query normally treats all matching rows as one group.
HAVING keeps or removes whole grouped result rows. Use WHERE when you need to decide which source rows participate in the grouping.
HAVING filters grouped or aggregate query results after GROUP BY and aggregate calculations have been performed.
WHERE filters individual source rows before grouping. HAVING filters grouped or aggregate results after grouping.
Yes. A common pattern is GROUP BY followed by HAVING COUNT(*) greater than, less than or equal to a chosen threshold.
Yes. WHERE can reduce the source rows first, then HAVING can filter the grouped aggregate results.
MySQL allows SELECT aliases such as total to be referenced in HAVING, although repeating the aggregate expression can be more portable across database systems.
MySQL permits HAVING in some aggregate queries without an explicit GROUP BY. In that case, the qualifying result can be treated as one aggregate group.
GROUP BY the candidate duplicate column and use HAVING COUNT(*) greater than 1.
Author & Instructor at plus2net
I write and maintain practical tutorials on Python, PHP, SQL, JavaScript, HTML, jQuery, and web development at plus2net. The tutorials focus on clear explanations, working examples, and code that readers can test and adapt while learning.
| ajay sarwai | 09-04-2011 |
| In mysql query SELECT category, type , count( * ) as total FROM `documents` GROUP BY category, type HAVING total < 3 is running perfectly. | |
| Rajesh | 19-10-2011 |
| EMp_ ID EMP Name Manga_ID 1 A 2 B 1 3 C 1 4 D 3 5 E 2 O/p: Emp_name ManagerName A B A C A D C E B What will be the query to get this output. | |
| smo1234 | 19-11-2011 |
| You can use inner join in this case to link same table. | |