SQL HAVING Clause
SQL HAVING filters groups after GROUP BY and aggregation. It is commonly used with aggregate functions such as COUNT(), SUM(), AVG(), MIN() and MAX() to keep only the groups that meet a condition.
What Is HAVING in SQL?
SQL HAVING filters the groups created by GROUP BY after the aggregate values have been calculated. Unlike WHERE, which filters individual rows before grouping, HAVING filters the resulting groups. Here is "which departments have more than 12 employees?":
SELECT department,
COUNT(*) AS employee_count
FROM employees
GROUP BY department
HAVING COUNT(*) > 12;
▶ Run this query in the SQL Playground
| department | employee_count |
|---|---|
| Customer Success | 15 |
| Legal | 16 |
| Marketing | 13 |
| Operations | 13 |
GROUP BY departmentcreates the groups — the 120 employee rows become 10 groups, one per department.COUNT(*)calculates a value for each group — the number of employees in it.HAVING COUNT(*) > 12filters those groups — only the 4 departments whose count is above 12 are returned.
What HAVING Kept and What It Removed
Run the same query without the HAVING line and you get all ten groups. HAVING tests each of them once:
| department | employee_count | HAVING COUNT(*) > 12 |
|---|---|---|
| Customer Success | 15 | ✓ kept |
| Design | 10 | ✗ removed |
| Engineering | 11 | ✗ removed |
| Finance | 10 | ✗ removed |
| HR | 12 | ✗ removed |
| Legal | 16 | ✓ kept |
| Marketing | 13 | ✓ kept |
| Operations | 13 | ✓ kept |
| Product | 9 | ✗ removed |
| Sales | 11 | ✗ removed |
HAVING never looks at a single employee. It only sees the ten summary rows, and it can test them with a number — the count — that did not exist until the groups were built.
HAVING builds directly on two earlier lessons: how GROUP BY creates SQL groups and how WHERE filters rows before aggregation.
SQL HAVING Syntax
HAVING is written after GROUP BY and before ORDER BY, followed by a condition that is tested once per group:
SELECT column_name,
aggregate_function(column)
FROM table_name
WHERE row_condition -- optional: filter rows first
GROUP BY column_name
HAVING group_condition -- filter the groups
ORDER BY column_name; -- optional: sort what is left
COUNT(*) > 12.What a HAVING Condition Can Contain
- An aggregate compared with a value —
HAVING SUM(total) > 30000. - An aggregate that is not in the SELECT list —
SELECT department … HAVING COUNT(*) > 12returns the same 4 departments without showing the count. - Any comparison operator —
=,<>,>=,BETWEEN,IN.HAVING COUNT(*) BETWEEN 10 AND 11returns 4 departments. - Several conditions joined with
AND/OR— see multiple conditions. - A subquery — see HAVING with a subquery.
- A column that is in GROUP BY — allowed, but that kind of condition usually belongs in WHERE (why).
Where HAVING Goes: Clause Order
SELECT department, COUNT(*)
FROM employees
HAVING COUNT(*) > 12
GROUP BY department;
near "GROUP": syntax error. Other databases also reject it.SELECT department, COUNT(*)
FROM employees
GROUP BY department
HAVING COUNT(*) > 12;
GROUP BY and HAVING Together
GROUP BY makes the groups. HAVING decides which of them you see.
Step 1 — group. The orders table has 200 rows. Grouping by status turns them into 6 summary rows:
SELECT status,
COUNT(*) AS orders
FROM orders
GROUP BY status;
▶ Run this query in the SQL Playground
| status | orders |
|---|---|
| cancelled | 27 |
| delivered | 82 |
| pending | 23 |
| processing | 28 |
| refunded | 19 |
| shipped | 21 |
Step 2 — filter the groups. Adding one line keeps only the statuses with at least 25 orders:
SELECT status,
COUNT(*) AS orders
FROM orders
GROUP BY status
HAVING COUNT(*) >= 25;
▶ Run this query in the SQL Playground
| status | orders |
|---|---|
| cancelled | 27 |
| delivered | 82 |
| processing | 28 |
200 rows became 6 groups, and HAVING kept 3 of them. Every HAVING query follows this shape — the only things that change are the grouping column, the aggregate and the threshold. If the grouping step itself is new to you, start with the SQL GROUP BY lesson; this page concentrates on the filter.
HAVING with Aggregate Functions
The aggregate you choose decides what question the filter answers.
HAVING with COUNT()
To use HAVING with COUNT, group the rows and compare COUNT(*) — the number of rows in each group — with a threshold. Which customers have placed four or more orders?
SELECT user_id,
COUNT(*) AS order_count
FROM orders
GROUP BY user_id
HAVING COUNT(*) >= 4
ORDER BY order_count DESC;
▶ Run this query in the SQL Playground
| user_id | order_count |
|---|---|
| 77 | 5 |
| 37 | 5 |
| 124 | 4 |
| 118 | 4 |
| 83 | 4 |
| 54 | 4 |
114 customers have at least one order; 6 of them reach four. Typical use: repeat customers, busy stores, and — with HAVING COUNT(*) > 1 — duplicate values.
COUNT(*) vs COUNT(column) in HAVING
COUNT(*) counts every row in the group. COUNT(column) counts only the rows where that column is not NULL. In the employees table, manager_id is NULL for 10 people who have no manager, so the two counts differ in some departments — and HAVING can test exactly that:
SELECT department,
COUNT(*) AS employees,
COUNT(manager_id) AS with_manager
FROM employees
GROUP BY department
HAVING COUNT(manager_id) < COUNT(*);
▶ Run this query in the SQL Playground
| department | employees | with_manager |
|---|---|---|
| Customer Success | 15 | 14 |
| Design | 10 | 8 |
| Engineering | 11 | 9 |
| HR | 12 | 11 |
| Legal | 16 | 13 |
| Operations | 13 | 12 |
These 6 departments each contain at least one employee without a manager. The other 4 have equal counts and are filtered out. The choice matters whenever a column can be NULL: HAVING COUNT(shipped_date) = 0 on orders grouped by status returns the 4 statuses in which no order has shipped, whereas HAVING COUNT(*) = 0 can never return anything, because a group always has at least one row.
HAVING with SUM()
SUM(column) adds up a numeric column inside each group; HAVING keeps the groups whose total passes the test. Which shipping cities have received more than 30,000 in orders?
SELECT shipping_city,
ROUND(SUM(total), 2) AS total_revenue
FROM orders
GROUP BY shipping_city
HAVING SUM(total) > 30000
ORDER BY total_revenue DESC;
▶ Run this query in the SQL Playground
| shipping_city | total_revenue |
|---|---|
| Jacksonville | 36302.64 |
| Los Angeles | 35977.73 |
| Columbus | 35480.28 |
| New York | 31667.49 |
| Seattle | 31265.78 |
5 of 20 cities pass. Typical use: revenue thresholds, budget limits, customers above a spending level. Order totals in the practice database are plain numbers with no currency attached.
HAVING with AVG()
AVG(column) returns the mean of each group. Which product categories have an average price above 1,100?
SELECT category,
ROUND(AVG(price), 2) AS average_price
FROM products
GROUP BY category
HAVING AVG(price) > 1100;
▶ Run this query in the SQL Playground
| category | average_price |
|---|---|
| Automotive | 1161.09 |
| Clothing | 1202.85 |
| Food & Beverage | 1249.46 |
| Home & Garden | 1129.71 |
The filter uses the unrounded average; ROUND(…, 2) only tidies the displayed value. AVG ignores NULLs rather than counting them as zero. Typical use: above-average segments, quality scores, average order value.
HAVING with MIN() and MAX()
MIN and MAX turn a statement about every row, or about at least one row, into a group filter. HAVING MIN(salary) > 60000 keeps departments where everyone earns more than 60,000 — if the lowest salary clears the bar, all of them do:
SELECT department,
MIN(salary) AS lowest_salary
FROM employees
GROUP BY department
HAVING MIN(salary) > 60000;
▶ Run this query in the SQL Playground
| department | lowest_salary |
|---|---|
| Customer Success | 61774 |
| Legal | 72975 |
| Marketing | 62910 |
| Product | 64930 |
HAVING MAX(salary) > 200000 keeps departments where at least one person earns more than 200,000:
SELECT department,
MAX(salary) AS highest_salary
FROM employees
GROUP BY department
HAVING MAX(salary) > 200000;
▶ Run this query in the SQL Playground
| department | highest_salary |
|---|---|
| Customer Success | 203999 |
| Marketing | 204449 |
| Operations | 203467 |
Typical use: "all items in the group meet a minimum" (MIN), "the group contains at least one large value" (MAX). MIN and MAX also work on dates and text. For what each function calculates in detail, see SQL aggregate functions.
WHERE vs HAVING
WHERE filters individual rows before they are grouped. HAVING filters whole groups after the aggregates have been calculated. A condition about a column value goes in WHERE; a condition about a count, total or average goes in HAVING.
| WHERE | HAVING | |
|---|---|---|
| Filters | Individual rows | Groups |
| Applied | Before GROUP BY | After GROUP BY and aggregation |
| Aggregate functions | Not allowed | Allowed — the usual reason to use it |
| Effect on the numbers | Changes what is counted or summed | Does not change any aggregate; only removes groups |
| Example | WHERE status = 'success' | HAVING SUM(amount) > 20000 |
Using WHERE and HAVING in One Query
They are not alternatives — most real reports use both. Counting successful payments only, which payment methods collected more than 20,000?
SELECT method,
COUNT(*) AS payments,
ROUND(SUM(amount), 2) AS collected
FROM payments
WHERE status = 'success'
GROUP BY method
HAVING SUM(amount) > 20000
ORDER BY collected DESC;
▶ Run this query in the SQL Playground
| method | payments | collected |
|---|---|---|
| Cryptocurrency | 24 | 28333.72 |
| Debit Card | 21 | 24174.39 |
| Bank Transfer | 23 | 22173.03 |
What happens at each stage:
- FROM reads the 200 rows of
payments. - WHERE keeps the 137 rows whose status is
'success'. Failed, pending and refunded payments are gone before anything is added up. - GROUP BY puts those rows into 8 groups, one per method, and
SUM(amount)is calculated for each. - HAVING keeps the 3 groups whose total is above 20,000.
- ORDER BY sorts the surviving groups, largest total first.
| Clause | Works on | Used for |
|---|---|---|
| WHERE | Rows | Row-level filtering — SQL WHERE clause |
| GROUP BY | Rows into groups | Aggregation — SQL GROUP BY |
| HAVING | Groups | Aggregate / group filtering |
| ORDER BY | Final result | Sorting — how ORDER BY sorts grouped results |
Why WHERE Cannot Filter an Aggregate
SELECT method, SUM(amount)
FROM payments
WHERE SUM(amount) > 20000
GROUP BY method;
misuse of aggregate: SUM(). MySQL, PostgreSQL and SQL Server reject it too.SELECT method, SUM(amount)
FROM payments
GROUP BY method
HAVING SUM(amount) > 20000;
WHERE looks at one row at a time, and a single payment has no "total per method". That number only exists after grouping, which is where HAVING works.
WHERE Changes the Numbers, HAVING Does Not
Moving a condition between the two clauses changes the question. Here WHERE keeps only orders above 4,000 (47 of 200), so COUNT(*) counts big orders, and HAVING keeps cities with four or more of them:
SELECT shipping_city,
COUNT(*) AS big_orders
FROM orders
WHERE total > 4000
GROUP BY shipping_city
HAVING COUNT(*) >= 4
ORDER BY big_orders DESC;
▶ Run this query in the SQL Playground
| shipping_city | big_orders |
|---|---|
| New York | 5 |
| Seattle | 4 |
| Los Angeles | 4 |
| Charlotte | 4 |
Remove the WHERE line and COUNT(*) counts every order, so all 20 cities pass HAVING COUNT(*) >= 4. Same HAVING, different answer — because WHERE decided what was being counted.
A Condition on the Grouped Column: WHERE or HAVING?
HAVING may also test a column that appears in GROUP BY. HAVING department IN ('Legal', 'Sales') and WHERE department IN ('Legal', 'Sales') return the same 2 rows in the Playground. Prefer WHERE: it states the intent — "only these rows" — and discards the unwanted rows before the database groups and aggregates them, instead of building groups that are then thrown away. Keep HAVING for conditions that need an aggregate.
HAVING with Multiple Conditions (AND / OR)
HAVING accepts several conditions combined with AND and OR, exactly as WHERE does. Each condition can use a different aggregate, and all of them are tested against the same group.
HAVING with AND — every condition must be true
Which departments are both large (12 or more people) and well paid (average above 125,000)?
SELECT department,
COUNT(*) AS employees,
ROUND(AVG(salary)) AS avg_salary
FROM employees
GROUP BY department
HAVING COUNT(*) >= 12
AND AVG(salary) > 125000;
▶ Run this query in the SQL Playground
| department | employees | avg_salary |
|---|---|---|
| Customer Success | 15 | 134140 |
| Operations | 13 | 126879 |
5 departments have 12 or more employees, but only 2 of them also clear the salary test.
HAVING with OR — one true condition is enough
Which departments are either very large or very well paid?
SELECT department,
COUNT(*) AS employees,
ROUND(AVG(salary)) AS avg_salary
FROM employees
GROUP BY department
HAVING COUNT(*) >= 15
OR AVG(salary) > 130000;
▶ Run this query in the SQL Playground
| department | employees | avg_salary |
|---|---|---|
| Customer Success | 15 | 134140 |
| Design | 10 | 133965 |
| Engineering | 11 | 131028 |
| Legal | 16 | 112866 |
Legal qualifies on headcount alone; Design and Engineering qualify on salary alone.
AND is evaluated before OR, so A OR B AND C means A OR (B AND C). Add parentheses to say what you mean. In the Playground, HAVING (COUNT(*) >= 15 OR AVG(salary) > 130000) AND MAX(salary) > 195000 returns 2 departments, while the same line without parentheses returns 3.HAVING with Column Aliases
Whether HAVING can use a column alias depends on the database. SQLite and MySQL accept an alias defined in SELECT; PostgreSQL and SQL Server do not, and require the aggregate expression to be repeated.
The Playground runs SQLite, so this works there and returns the same four departments as the first example:
SELECT department,
COUNT(*) AS employee_count
FROM employees
GROUP BY department
HAVING employee_count > 12;
▶ Run this query in the SQL Playground
| department | employee_count |
|---|---|
| Customer Success | 15 |
| Legal | 16 |
| Marketing | 13 |
| Operations | 13 |
| Database | HAVING employee_count > 12 | HAVING COUNT(*) > 12 |
|---|---|---|
| SQLite | Works | Works |
| MySQL | Works | Works |
| PostgreSQL | Error — the alias is not recognised as a column | Works |
| SQL Server | Error — invalid column name | Works |
The reason is the logical order of a query: HAVING is evaluated before the SELECT list, so in standard SQL the alias does not exist yet. SQLite and MySQL look the alias up anyway as a convenience.
HAVING COUNT(*) > 12. It runs on all four databases, which is why the examples in this lesson are written that way. Naming columns with AS is covered in SQL aliases.HAVING with JOIN
HAVING works on joined tables exactly as it does on one table: the JOIN builds the rows, GROUP BY groups them, and HAVING filters the groups. A customer's home city lives in users and their orders in orders, so "which customer cities generated 12 or more orders?" needs both:
SELECT u.city,
COUNT(o.id) AS total_orders,
ROUND(SUM(o.total), 2) AS revenue
FROM users u
JOIN orders o
ON o.user_id = u.id
GROUP BY u.city
HAVING COUNT(o.id) >= 12
ORDER BY total_orders DESC;
▶ Run this query in the SQL Playground
| city | total_orders | revenue |
|---|---|---|
| Indianapolis | 24 | 62099.26 |
| Phoenix | 18 | 60179.7 |
| Columbus | 17 | 30743.66 |
| San Antonio | 13 | 32576.37 |
| Houston | 13 | 39685.75 |
| Jacksonville | 12 | 30440.57 |
- JOIN attaches each order to the customer who placed it.
- GROUP BY u.city builds one group per customer city.
- HAVING COUNT(o.id) >= 12 keeps the 6 cities with enough orders.
Qualify the columns (o.id, u.city) so it is clear which table each aggregate counts. How the tables are combined is covered in SQL JOINs.
LEFT JOIN + HAVING: Finding Rows with No Match
A LEFT JOIN keeps customers who have no orders, with NULL in the order columns. Because COUNT(o.id) skips NULLs, it is 0 for exactly those customers — which makes HAVING COUNT(o.id) = 0 a way to find them:
SELECT u.id,
u.first_name,
u.last_name,
COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o
ON o.user_id = u.id
GROUP BY u.id, u.first_name, u.last_name
HAVING COUNT(o.id) = 0
ORDER BY u.id
LIMIT 5;
▶ Run this query in the SQL Playground
| id | first_name | last_name | order_count |
|---|---|---|---|
| 3 | Kenneth | Young | 0 |
| 9 | Sandra | Jackson | 0 |
| 12 | Richard | Thomas | 0 |
| 14 | Jeffrey | Lopez | 0 |
| 17 | Stephanie | Mitchell | 0 |
Without the LIMIT the query returns 36 of the 150 customers. This is the COUNT(*) vs COUNT(column) difference at work: HAVING COUNT(*) = 0 returns 0 rows here, because the LEFT JOIN still produces one row for a customer without orders. See LEFT JOIN for how unmatched rows are kept.
HAVING Without GROUP BY
Yes — HAVING can be used without GROUP BY when the query uses an aggregate function. With no GROUP BY, all the rows form a single implicit group, so HAVING either keeps that one summary row or returns nothing.
SELECT COUNT(*) AS total_orders
FROM orders
HAVING COUNT(*) > 100;
▶ Run this query in the SQL Playground
| total_orders |
|---|
| 200 |
Raise the threshold above the real count and the single group fails the test:
SELECT COUNT(*) AS total_orders
FROM orders
HAVING COUNT(*) > 500;
▶ Run this query in the SQL Playground
This is occasionally useful as an all-or-nothing check ("return the total only if there are enough rows"). It is not a way to filter rows.
What Differs Between Databases
- The aggregate form above runs in SQLite (version 3.39 and later, which includes the Playground), MySQL, PostgreSQL and SQL Server.
- HAVING on a plain column with no aggregate and no GROUP BY is rejected by SQLite, PostgreSQL and SQL Server. MySQL accepts it and treats it like a row filter, which is a MySQL extension.
SELECT id, total
FROM orders
HAVING total > 4900;
HAVING clause on a non-aggregate querySELECT id, total
FROM orders
WHERE total > 4900;
HAVING with a Subquery or CASE
Two patterns for when a fixed number is not enough.
Compare Each Group with an Overall Value
The threshold does not have to be typed in. A subquery can calculate it — here, the average salary of the whole company (124371):
SELECT department,
ROUND(AVG(salary)) AS avg_salary
FROM employees
GROUP BY department
HAVING AVG(salary) > (SELECT AVG(salary) FROM employees)
ORDER BY avg_salary DESC;
▶ Run this query in the SQL Playground
| department | avg_salary |
|---|---|
| Customer Success | 134140 |
| Design | 133965 |
| Engineering | 131028 |
| Operations | 126879 |
The subquery runs on all employees; the outer AVG(salary) runs once per department. More patterns like this are in SQL subqueries.
Count Only Some Rows in Each Group
A CASE expression inside an aggregate counts just the rows that match a condition, without removing the others from the group. Which cities have four or more cancelled or refunded orders — shown alongside all of their orders?
SELECT shipping_city,
COUNT(*) AS orders,
SUM(CASE WHEN status IN ('cancelled', 'refunded') THEN 1 ELSE 0 END) AS lost_orders
FROM orders
GROUP BY shipping_city
HAVING SUM(CASE WHEN status IN ('cancelled', 'refunded') THEN 1 ELSE 0 END) >= 4
ORDER BY lost_orders DESC;
▶ Run this query in the SQL Playground
| shipping_city | orders | lost_orders |
|---|---|---|
| New York | 12 | 7 |
| Seattle | 11 | 4 |
| Phoenix | 9 | 4 |
| Columbus | 12 | 4 |
A WHERE filter could not produce this result: it would remove the other orders, and the orders column would no longer show each city's full count. See CASE WHEN for the expression itself.
ROW_NUMBER() or RANK() has to go in an outer query that wraps the first one. See SQL window functions.SQL Execution Order: Where HAVING Runs
A query is written starting with SELECT, but it is logically processed in a different order. HAVING is the fourth step — after the groups exist, before the SELECT list is produced:
- FROM / JOINRead the tables and combine them.
- WHERERemove rows that fail the row condition.
- GROUP BYPut the remaining rows into groups and calculate the aggregates.
- HAVINGRemove groups that fail the group condition.
- SELECTProduce the output columns and their aliases.
- ORDER BYSort the result.
- LIMITKeep the first N rows.
This order explains the rules in this lesson:
- WHERE cannot use an aggregate — step 2 happens before the groups of step 3 exist.
- HAVING can use an aggregate — by step 4 every group has its counts and totals.
- An alias is not guaranteed to work in HAVING — aliases are created in step 5. SQLite and MySQL resolve them early; PostgreSQL and SQL Server do not.
- ORDER BY can always use an alias — it runs after SELECT.
Common HAVING Mistakes
Ten errors, each with its correction.
1. Filtering an aggregate with WHERE
WHERE COUNT(*) > 12 is an error in every database. Fix: move the condition after GROUP BY and write HAVING COUNT(*) > 12. See WHERE vs HAVING.
2. Using HAVING for a plain row filter
GROUP BY department HAVING department = 'Legal' runs, but it builds every group and then discards all but one. Fix: WHERE department = 'Legal' before the GROUP BY.
3. Forgetting GROUP BY
SELECT department, COUNT(*)
FROM employees
HAVING COUNT(*) > 12;
SELECT department, COUNT(*)
FROM employees
GROUP BY department
HAVING COUNT(*) > 12;
Without GROUP BY the whole table is one group. PostgreSQL, SQL Server and MySQL (in its default mode) reject the query because department is neither grouped nor aggregated. SQLite runs it and returns a single row — one arbitrary department next to the whole-table count of 120 — which looks like an answer but is not one.
4. Writing HAVING before GROUP BY
The clause order is fixed: … GROUP BY … HAVING … ORDER BY …. Putting HAVING first is a syntax error. Fix: see clause order.
5. Confusing HAVING with ORDER BY
HAVING removes groups; it does not sort them or pick "the top N". The three largest departments is not a HAVING question, because you do not know the threshold in advance. Fix: ORDER BY employees DESC LIMIT 3, which returns Legal (16), Customer Success (15), Operations (13). Use HAVING when the rule is a threshold, and ORDER BY with LIMIT when it is a ranking.
6. Using the wrong aggregate function
"Customers who spent more than 10,000" means their orders added together: HAVING SUM(total) > 10000 returns 7 customers. HAVING MAX(total) > 10000 asks whether any single order was that large and returns 0; HAVING AVG(total) > 4000 is a third, different question and returns 21. Fix: say the condition in words first — "in total", "on average", "at least one", "how many" — and pick SUM, AVG, MAX or COUNT to match.
7. Grouping by the wrong column
HAVING can only be as correct as the groups it filters. In the Playground, 34 product names are used by more than one product, so GROUP BY name merges different products into one group and inflates its totals. Fix: group by the key — GROUP BY id, name — and keep the label for display.
8. Putting a bare, ungrouped column in HAVING
SELECT department, COUNT(*)
FROM employees
GROUP BY department
HAVING salary > 100000;
SELECT department, COUNT(*)
FROM employees
GROUP BY department
HAVING AVG(salary) > 100000;
A department has many salaries, so salary on its own is not a property of the group. PostgreSQL, SQL Server and MySQL raise an error. SQLite does not: it tests the salary of one arbitrary employee per department and returns 8 departments, a result with no reliable meaning. Fix: wrap the column in the aggregate you intend — AVG, MIN or MAX.
9. Assuming COUNT(column) counts every row
COUNT(column) skips NULLs, so HAVING COUNT(manager_id) >= 12 and HAVING COUNT(*) >= 12 can keep different departments. Fix: use COUNT(*) for "how many rows" and COUNT(column) only when you mean "how many rows have a value". See COUNT(*) vs COUNT(column).
10. Assuming every database behaves the same
The Playground runs SQLite, which is more permissive than most. These are the HAVING behaviours that differ:
| Behaviour | SQLite | MySQL | PostgreSQL | SQL Server |
|---|---|---|---|---|
| Aggregate in WHERE | Error | Error | Error | Error |
| SELECT alias in HAVING | Allowed | Allowed | Not allowed | Not allowed |
| HAVING without GROUP BY, with an aggregate | Allowed (3.39+) | Allowed | Allowed | Allowed |
| HAVING on a plain column, no aggregate, no GROUP BY | Error | Allowed (acts as a row filter) | Error | Error |
| Ungrouped, unaggregated column in HAVING | Allowed (arbitrary row) | Error | Error | Error |
Fix: write the portable form — GROUP BY present, aggregates repeated in HAVING instead of aliases, every HAVING column either grouped or aggregated. A query written that way gives the same result on all four.
SQL HAVING Examples for Data Analysts
Eight business questions, answered on the Playground's e-commerce data.
Each example gives the question, the query, its real output and the idea to take away. Open one in the Playground, change the threshold and run it again — the fastest way to get a feel for HAVING is to watch groups appear and disappear.
Example 1 Which customers have placed four or more orders?
The retention team wants its repeat buyers by name, with what each has spent.
SELECT u.first_name, u.last_name,
COUNT(o.id) AS order_count,
ROUND(SUM(o.total), 2) AS total_spent
FROM users u
JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.first_name, u.last_name
HAVING COUNT(o.id) >= 4
ORDER BY order_count DESC, total_spent DESC;
▶ Run this query in the SQL Playground
| first_name | last_name | order_count | total_spent |
|---|---|---|---|
| Joseph | King | 5 | 14593.17 |
| Andrew | Wright | 5 | 6432.94 |
| Paul | Young | 4 | 15384.26 |
| Michael | Rodriguez | 4 | 12059.32 |
| Laura | Martin | 4 | 11780.53 |
| Steven | Rivera | 4 | 9700.36 |
6 customers qualify. Grouping by u.id as well as the name keeps two customers who share a name in separate groups.
Key concept: HAVING COUNT() for a frequency threshold
Example 2 Which product names are listed more than once?
A data-quality check before a catalogue import: any name that appears on several products needs a look.
SELECT name,
COUNT(*) AS listings
FROM products
GROUP BY name
HAVING COUNT(*) > 1
ORDER BY listings DESC, name
LIMIT 5;
▶ Run this query in the SQL Playground
| name | listings |
|---|---|
| Artisan Hot Sauce Trio | 5 |
| Ergonomic Desk Mat XL | 4 |
| Giant Stuffed Teddy Bear | 4 |
| Sticky Notes Assorted 18pk | 4 |
| The Manager's Path | 4 |
Without the LIMIT, 34 names are returned. Group by the column that should be unique and keep the groups with more than one row.
Key concept: HAVING COUNT(*) > 1 finds duplicates
Example 3 Which categories earned more than 15,000 from delivered orders?
Finance only recognises revenue once an order is delivered, and wants the categories above 15,000.
SELECT p.category,
ROUND(SUM(oi.subtotal), 2) AS delivered_revenue
FROM order_items oi
JOIN products p ON p.id = oi.product_id
JOIN orders o ON o.id = oi.order_id
WHERE o.status = 'delivered'
GROUP BY p.category
HAVING SUM(oi.subtotal) > 15000
ORDER BY delivered_revenue DESC;
▶ Run this query in the SQL Playground
| category | delivered_revenue |
|---|---|
| Office Supplies | 22858.48 |
| Food & Beverage | 19930.66 |
| Books | 19514.61 |
| Automotive | 18032.88 |
WHERE removes undelivered orders before anything is added up; HAVING then keeps 4 of the 10 categories.
Key concept: WHERE for the rows, HAVING for the total
Example 4 Which products average 3.5 stars or better across at least three reviews?
Merchandising wants well-rated products, but one five-star review should not be enough to qualify.
SELECT p.name,
COUNT(r.id) AS reviews,
ROUND(AVG(r.rating), 2) AS avg_rating
FROM products p
JOIN reviews r ON r.product_id = p.id
GROUP BY p.id, p.name
HAVING COUNT(r.id) >= 3
AND AVG(r.rating) >= 3.5
ORDER BY avg_rating DESC, reviews DESC;
▶ Run this query in the SQL Playground
| name | reviews | avg_rating |
|---|---|---|
| Deep Work (Newport) | 3 | 4.33 |
| Bike Helmet MIPS | 3 | 4 |
| Soy Wax Candle Set | 3 | 4 |
| A5 Hardcover Notebook 3pk | 6 | 3.5 |
The COUNT condition is a minimum sample size; the AVG condition is the quality bar. Both must hold for the same product.
Key concept: Guard an average with a minimum count
Example 5 Which customers have spent more than 11,000 in total?
Account management is building a high-value customer list.
SELECT u.first_name, u.last_name,
COUNT(o.id) AS orders,
ROUND(SUM(o.total), 2) AS total_spent
FROM users u
JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.first_name, u.last_name
HAVING SUM(o.total) > 11000
ORDER BY total_spent DESC;
▶ Run this query in the SQL Playground
| first_name | last_name | orders | total_spent |
|---|---|---|---|
| Paul | Young | 4 | 15384.26 |
| Joseph | King | 5 | 14593.17 |
| Michael | Rodriguez | 4 | 12059.32 |
| Amanda | Davis | 3 | 11837.52 |
| Laura | Martin | 4 | 11780.53 |
| Deborah | Jones | 3 | 11528.59 |
Two of these 6 customers got there with only three orders — which is why this list differs from the "four or more orders" list in Example 1.
Key concept: HAVING SUM() for a value threshold
Example 6 Which payment methods were used more than 25 times?
The payments team wants the high-volume methods, with how many of those payments failed.
SELECT method,
COUNT(*) AS payments,
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed
FROM payments
GROUP BY method
HAVING COUNT(*) > 25
ORDER BY payments DESC;
▶ Run this query in the SQL Playground
| method | payments | failed |
|---|---|---|
| Bank Transfer | 38 | 3 |
| Cryptocurrency | 34 | 5 |
| Debit Card | 28 | 1 |
| Apple Pay | 27 | 2 |
4 of the 8 methods pass. The failed column is a conditional count: it is shown for each kept group but plays no part in the filter.
Key concept: Filter on one aggregate, report another
Example 7 Which months had nine or more orders?
Operations wants the peak months for staffing.
SELECT strftime('%Y-%m', order_date) AS month,
COUNT(*) AS orders,
ROUND(SUM(total), 2) AS revenue
FROM orders
GROUP BY strftime('%Y-%m', order_date)
HAVING COUNT(*) >= 9
ORDER BY month;
▶ Run this query in the SQL Playground
| month | orders | revenue |
|---|---|---|
| 2022-12 | 9 | 27670.22 |
| 2023-05 | 9 | 26116.83 |
| 2023-12 | 11 | 29761.96 |
| 2024-04 | 9 | 22604.85 |
The group is an expression, not a stored column. strftime is SQLite's function; MySQL uses DATE_FORMAT and PostgreSQL to_char — see SQL date functions.
Key concept: HAVING works on groups built from an expression
Example 8 Which cities combine order volume with a high average order value?
Marketing is choosing cities for a premium campaign: at least 10 orders, averaging more than 2,600 each.
SELECT shipping_city,
COUNT(*) AS orders,
ROUND(AVG(total), 2) AS avg_order_value
FROM orders
GROUP BY shipping_city
HAVING COUNT(*) >= 10
AND AVG(total) > 2600
ORDER BY avg_order_value DESC;
▶ Run this query in the SQL Playground
| shipping_city | orders | avg_order_value |
|---|---|---|
| Columbus | 12 | 2956.69 |
| Seattle | 11 | 2842.34 |
| Indianapolis | 10 | 2755.51 |
| Charlotte | 11 | 2711.39 |
| New York | 12 | 2638.96 |
5 of 20 cities meet both conditions. Cities with a higher average but fewer than 10 orders are left out on purpose.
Key concept: Multiple aggregate conditions with AND
SQL HAVING Practice Problems
9 exercises, beginner to challenge, on the real Playground tables.
Write each query yourself before opening the solution. Every solution has been run against the Playground database, and the expected result tells you what you should see.
-
1Beginner Which shipping cities have received more than 10 orders? Show the city and its order count, largest first. Schema:
orders (id, shipping_city, total, status)· Concept: HAVING COUNT(*)Show solution
SELECT shipping_city, COUNT(*) AS order_count FROM orders GROUP BY shipping_city HAVING COUNT(*) > 10 ORDER BY order_count DESC;▶ Run this query in the SQL Playground
Expected result: 8 cities are returned. Jacksonville is first with 15 orders. A city with exactly 10 orders is excluded, because the condition is
> 10, not>= 10. -
2Beginner Which product categories contain 12 or more products? Schema:
products (id, name, category, price)· Concept: HAVING COUNT(*)Show solution
SELECT category, COUNT(*) AS products FROM products GROUP BY category HAVING COUNT(*) >= 12 ORDER BY products DESC;▶ Run this query in the SQL Playground
Expected result: 5 of the 10 categories qualify; the largest is Food & Beverage with 17 products.
-
3Intermediate Which departments have a total payroll above 1,500,000? Schema:
employees (id, department, salary)· Concept: HAVING SUM()Show solution
SELECT department, SUM(salary) AS total_salary FROM employees GROUP BY department HAVING SUM(salary) > 1500000 ORDER BY total_salary DESC;▶ Run this query in the SQL Playground
Expected result: SUM adds the salaries inside each department; 4 departments pass. Customer Success has the largest payroll (2012099).
-
4Intermediate Looking at successful payments only, which payment methods have an average payment above 1,000? Schema:
payments (id, amount, method, status)· Concept: WHERE + HAVING AVG()Show solution
SELECT method, COUNT(*) AS payments, ROUND(AVG(amount), 2) AS avg_payment FROM payments WHERE status = 'success' GROUP BY method HAVING AVG(amount) > 1000 ORDER BY avg_payment DESC;▶ Run this query in the SQL Playground
Expected result:
status = 'success'is a row condition, so it goes in WHERE; the average is a group condition, so it goes in HAVING. 5 methods are returned, led by Google Pay (1264.63). -
5Intermediate Which job titles are held by five or more employees? Show the headcount and the average salary for each. Schema:
employees (id, title, salary)· Concept: Filter on COUNT, report AVGShow solution
SELECT title, COUNT(*) AS employees, ROUND(AVG(salary)) AS avg_salary FROM employees GROUP BY title HAVING COUNT(*) >= 5 ORDER BY employees DESC;▶ Run this query in the SQL Playground
Expected result: 4 titles qualify; Senior CS Manager is the most common with 7 people. The average is displayed but not filtered on.
-
6Intermediate Which products have sold 25 or more units in total? Show the product id and units sold. Schema:
order_items (id, order_id, product_id, quantity, subtotal)· Concept: HAVING SUM() on a quantityShow solution
SELECT product_id, SUM(quantity) AS units_sold FROM order_items GROUP BY product_id HAVING SUM(quantity) >= 25 ORDER BY units_sold DESC;▶ Run this query in the SQL Playground
Expected result: Units are a SUM of
quantity, not a COUNT of rows — one order line can contain several units. 7 products qualify; product 38 leads with 37 units. -
7Advanced Which managers have 12 or more direct reports? Show each manager's name and number of reports. Schema:
employees (id, first_name, last_name, manager_id)· Concept: Self join + HAVINGShow solution
SELECT m.first_name, m.last_name, COUNT(e.id) AS direct_reports FROM employees e JOIN employees m ON m.id = e.manager_id GROUP BY m.id, m.first_name, m.last_name HAVING COUNT(e.id) >= 12 ORDER BY direct_reports DESC;▶ Run this query in the SQL Playground
Expected result: The table is joined to itself:
eis the employee,mis that employee's manager. 5 managers qualify; Michael Campbell has the most reports (14). The pattern is explained in SELF JOIN. -
8Advanced Which customers have paid with three or more different payment methods? Schema:
payments (id, user_id, method) · users (id, first_name, last_name)· Concept: HAVING COUNT(DISTINCT …)Show solution
SELECT u.first_name, u.last_name, COUNT(DISTINCT p.method) AS methods_used FROM users u JOIN payments p ON p.user_id = u.id GROUP BY u.id, u.first_name, u.last_name HAVING COUNT(DISTINCT p.method) >= 3 ORDER BY u.last_name, u.first_name;▶ Run this query in the SQL Playground
Expected result:
COUNT(DISTINCT method)counts each method once per customer, however many times it was used. 11 customers are returned. DISTINCT inside an aggregate is what separates "three payments" from "three different methods". -
9Challenge Which customer home cities have at least 10 orders and an average order value above the average of all orders? Schema:
users (id, city) · orders (id, user_id, total)· Concept: JOIN + two conditions + subqueryShow solution
SELECT u.city, COUNT(o.id) AS orders, ROUND(AVG(o.total), 2) AS avg_order_value FROM users u JOIN orders o ON o.user_id = u.id GROUP BY u.city HAVING COUNT(o.id) >= 10 AND AVG(o.total) > (SELECT AVG(total) FROM orders) ORDER BY avg_order_value DESC;▶ Run this query in the SQL Playground
Expected result: The subquery returns one number — the overall average order value — and each city's average is compared with it. 6 cities pass both tests; Phoenix is highest at 3343.32.
📝 Quiz — SQL HAVING
Test your understanding · 0/10 answered
What does the HAVING clause filter?
HAVING is tested once per group, after the aggregates have been calculated. Row filtering is the job of WHERE.
Which clause can contain the condition COUNT(*) > 5?
Aggregate functions are not allowed in WHERE, because WHERE runs before the groups exist. A condition on an aggregate goes in HAVING.
What is the correct order of clauses?
WHERE comes before GROUP BY, HAVING comes straight after it, and ORDER BY is last.
Which query returns the customers who have more than 3 orders?
Group the orders by customer, then keep the groups whose count is above 3. The other options put an aggregate in WHERE or place the clauses in the wrong order.
A query has WHERE status = 'delivered' and HAVING COUNT(*) >= 5. What does COUNT(*) count?
WHERE removes the non-delivered rows before grouping, so they are never counted. HAVING then tests the count of what is left.
HAVING MIN(salary) > 60000 keeps the departments where…
If the lowest salary in the group is above 60,000, all of them are. MAX would test "at least one".
SELECT city, COUNT(*) AS n FROM users GROUP BY city HAVING n > 5; — which statement is accurate?
Using a SELECT alias in HAVING is an extension offered by SQLite and MySQL. PostgreSQL and SQL Server need HAVING COUNT(*) > 5.
The orders table has 200 rows. What does SELECT COUNT(*) FROM orders HAVING COUNT(*) > 500; return?
Without GROUP BY the whole table is one group. Its count, 200, is not above 500, so that single group is filtered out and nothing is returned.
Which condition finds values that appear more than once in a column you have grouped by?
Group by the column and keep the groups with more than one row — the standard way to find duplicates.
A department has 12 employees, and manager_id is NULL for 2 of them. What is COUNT(manager_id) for that department?
COUNT(column) counts only the rows where the column is not NULL. COUNT(*) would return 12.
SQL HAVING Interview Questions
Six questions that come up often, with concise answers.
Q1. What is the difference between WHERE and HAVING?
WHERE filters individual rows before grouping and cannot contain aggregate functions. HAVING filters groups after aggregation and normally does contain one. In a query with both, WHERE decides which rows are aggregated and HAVING decides which aggregated groups are returned.
Q2. Can HAVING be used without GROUP BY?
Yes, in an aggregate query. The whole result is treated as a single group, so SELECT COUNT(*) FROM orders HAVING COUNT(*) > 100 returns either one row or none. Using HAVING to filter plain rows with no aggregate is an error in PostgreSQL, SQL Server and SQLite; only MySQL accepts it.
Q3. Can you use a column alias in HAVING?
It depends on the database. MySQL and SQLite allow an alias from the SELECT list; PostgreSQL and SQL Server do not, because HAVING is logically evaluated before SELECT. Repeating the aggregate expression works everywhere.
Q4. How do you find duplicate values with SQL?
Group by the column that should be unique and keep the groups with more than one row: SELECT name, COUNT(*) FROM products GROUP BY name HAVING COUNT(*) > 1;
Q5. Is it better to put a condition in WHERE or in HAVING?
If the condition does not need an aggregate, put it in WHERE: the rows are removed before grouping, so there is less to aggregate and the intent is clearer. Use HAVING only for conditions on aggregate values. A query optimizer may move a simple HAVING condition earlier on its own, but that should not be relied on.
Q6. Write a query for customers with more than 3 orders and more than 10,000 in total spending.
SELECT user_id, COUNT(*) AS orders, SUM(total) AS total_spent FROM orders GROUP BY user_id HAVING COUNT(*) > 3 AND SUM(total) > 10000; Group by customer, then require both aggregate conditions with AND. On the Playground data this returns 4 customers.
More practice across every topic: SQL interview questions and answers.
SQL HAVING Cheat Sheet
Ten patterns for quick reference.
| Goal | HAVING clause |
|---|---|
| Groups with more than N rows | HAVING COUNT(*) > 5 |
| Duplicate values | HAVING COUNT(*) > 1 |
| Groups with no matching rows (after a LEFT JOIN) | HAVING COUNT(o.id) = 0 |
| Total above a threshold | HAVING SUM(amount) > 20000 |
| Average above a threshold | HAVING AVG(price) > 1100 |
| Every row in the group above X | HAVING MIN(salary) > 60000 |
| At least one row above X | HAVING MAX(salary) > 200000 |
| Both conditions | HAVING COUNT(*) >= 12 AND AVG(salary) > 125000 |
| Distinct values per group | HAVING COUNT(DISTINCT method) >= 3 |
| Compare with an overall value | HAVING AVG(salary) > (SELECT AVG(salary) FROM employees) |
Complete Query Template
SELECT column_name, -- the group
aggregate_fn(column) -- the metric
FROM table_name
WHERE row_condition -- filter rows (no aggregates)
GROUP BY column_name -- build the groups
HAVING aggregate_fn(column) > 0 -- filter groups (aggregates allowed)
ORDER BY column_name -- sort the result
LIMIT 10; -- keep the first rows
Frequently Asked Questions
What is HAVING in SQL?
SQL HAVING filters the groups created by GROUP BY after the aggregate values have been calculated. It is commonly used with aggregate functions such as COUNT(), SUM(), AVG(), MIN() and MAX() to keep only the groups that meet a condition.
What is the syntax of HAVING?
The basic syntax is SELECT column, aggregate_function(column) FROM table GROUP BY column HAVING condition; HAVING is written after GROUP BY and before ORDER BY.
What is the difference between WHERE and HAVING?
WHERE filters individual rows before they are grouped and cannot contain aggregate functions. HAVING filters whole groups after aggregation. A query can use both: WHERE to choose the rows, HAVING to choose the groups.
Can HAVING be used without GROUP BY?
Yes, when the query uses an aggregate function. The whole result is then treated as one group, so the query returns either one row or no rows. Using HAVING on a plain column with no aggregate is an error in SQLite, PostgreSQL and SQL Server; MySQL accepts it as an extension.
Can HAVING use COUNT()?
Yes. HAVING COUNT(*) > 5 keeps the groups that contain more than five rows. COUNT(*) counts every row in the group, while COUNT(column) counts only the rows where that column is not NULL.
Can HAVING use SUM()?
Yes. HAVING SUM(amount) > 20000 keeps the groups whose total is above 20,000, for example payment methods or categories above a revenue threshold.
Can HAVING use AVG()?
Yes. HAVING AVG(price) > 1100 keeps the groups whose average is above 1,100. AVG ignores NULL values.
Can HAVING use multiple conditions?
Yes. Combine conditions with AND and OR, for example HAVING COUNT(*) >= 12 AND AVG(salary) > 125000. AND is evaluated before OR, so use parentheses when you mix them.
Can HAVING be used with JOIN?
Yes. The JOIN combines the tables first, GROUP BY groups the joined rows, and HAVING filters those groups. With a LEFT JOIN, HAVING COUNT(o.id) = 0 finds rows that have no match.
Can HAVING use a column alias?
It depends on the database. SQLite and MySQL allow a SELECT alias in HAVING. PostgreSQL and SQL Server do not, so the aggregate expression has to be repeated, which also works everywhere.
What is the difference between GROUP BY and HAVING?
GROUP BY creates the groups and lets an aggregate be calculated for each one. HAVING then removes the groups that do not meet a condition. GROUP BY changes the shape of the result; HAVING only decides which groups stay in it.
How do I practice HAVING queries?
Run the examples from this lesson in the free SQL Playground, change the thresholds, then work through the practice problems above and the HAVING challenges.