Tier 2 · Core SQL

HAVING in SQL

By Owen Middleton · Updated September 2026 · Examples run on PostgreSQL 17

Before this GROUP BY

What is HAVING in SQL?

HAVING filters groups after aggregation is complete. It decides which grouped results survive into the output.

You're querying a database with 62 customers and several hundred orders. The product team wants to identify repeat buyers — anyone who placed more than three orders. Getting there takes two steps: count orders per customer with GROUP BY, then keep only the groups where that count exceeds three. That second step is HAVING's job. You can't do it with WHERE, because WHERE runs before grouping starts, before any counts exist. HAVING runs after GROUP BY, when the aggregate values are ready to filter on.

The division is: WHERE filters individual rows going into the aggregation; HAVING filters groups coming out of it.

How do you filter by a COUNT with HAVING?

Here's what the repeat-buyer query looks like:

SELECT customer_id, COUNT(*) AS order_count
FROM orders
GROUP BY customer_id
HAVING COUNT(*) > 3

SQL groups orders by customer, counts the rows in each group, and HAVING removes any group where the count is 3 or under. Only repeat buyers reach the output.

HAVING works with any aggregate function. Products that generated more than $2,000 in total revenue:

SELECT product_id, SUM(quantity * unit_price) AS total_revenue
FROM order_items
GROUP BY product_id
HAVING SUM(quantity * unit_price) > 2000

Why can you not use a SELECT alias in HAVING?

Notice that the aggregate expression appears twice — once in SELECT and again in HAVING. That's required. SQL resolves aliases after HAVING has already run, which means you can't write HAVING total_revenue > 2000 even though you defined that alias in SELECT. Writing the full expression in both places is standard practice, and once you know why, it stops feeling strange.

What is the difference between WHERE and HAVING?

WHERE and HAVING work together when you need to filter at both stages:

SELECT customer_id, COUNT(*) AS delivered_count
FROM orders
WHERE status = 'delivered'
GROUP BY customer_id
HAVING COUNT(*) > 2

WHERE removes non-delivered orders before any grouping happens. GROUP BY forms the groups. HAVING keeps only customers with more than two delivered orders. That sequence is also the order SQL evaluates the query.

Why can you not put COUNT(*) in a WHERE clause?

The one thing that trips people up: putting aggregate conditions in WHERE instead of HAVING.

A filter like COUNT(*) > 3 or SUM(amount) > 1000 belongs in HAVING, not WHERE. When WHERE runs, there are no groups yet and no aggregate values. SQL raises an error.

The inverse mistake is less common but worth knowing: putting non-aggregate conditions in HAVING when they belong in WHERE. Something like HAVING status = 'delivered' is technically valid, but it forces SQL to group every row first and then discard the ones that don't match. Running that filter through WHERE before grouping is more efficient, and on large tables the difference shows.

Check your understanding

You want to keep only groups where SUM(total_amount) > 500. Where does that condition go?

Practice HAVING in SQL

Practice · easy ecommerce · Brightlane

Brightlane's growth team is analysing repeat-purchase behaviour to build a loyalty programme.

Write a query to return the customer ID and order count for every customer with more than three orders on record.

Assumptions:

  • The orders table contains every order Brightlane has processed.
  • The threshold is on the per-customer count of orders — three orders does not qualify; four or more does.
  • The condition applies to the per-customer count, not to individual orders.

Output:

  • One row per qualifying customer, with columns customer_id and order_count.
Schema · ecommerce5 tables? = nullable
categories
idinteger
nametext
parent_id?integer
products
idinteger
nametext
category_id?integer
pricenumeric
stock_qtyinteger
attributes?jsonb
order_items
idinteger
order_id?integer
product_id?integer
quantityinteger
unit_pricenumeric
customers
idinteger
nametext
emailtext
city?text
countrytext
created_attimestamptz
is_activeboolean
orders
idinteger
customer_id?integer
ordered_attimestamptz
statustext
total_amountnumeric

Run previews · Check grades

Write a query, then run it to see results here.

Worked solution

The full breakdown walks through the shape, each clause, why this approach beats the alternatives, and the trap to avoid.

See the full worked solution
In the game

Station Zero, our free browser SQL game, teaches this concept inside a story. No signup.

filter groups in Station Zero

10 HAVING practice problems

Start learning to practice all 10 HAVING problems, with instant grading and mastery tracking.

Common questions about HAVING

Can you use HAVING without GROUP BY?

Yes. With no GROUP BY the whole table counts as one group, so HAVING tests the aggregate over everything and returns either that single row or no rows at all. It is an unusual shape, but it is legal and sometimes the clearest way to ask whether a total clears a threshold.

Can HAVING filter on a column that is not in the SELECT list?

Yes. HAVING can test any aggregate over the grouped rows whether or not it appears in the output, so you can keep the customers whose total spend clears a threshold while returning only their id.

Should a filter on a plain column go in WHERE or HAVING?

WHERE. Both can be made to work, but WHERE removes rows before any grouping happens, so the database forms fewer groups and does less work. Save HAVING for conditions that need an aggregate, because those do not exist until the grouping is finished.

How you actually get good at SQL

Reading explains SQL. Writing it, over and over with instant feedback, is what makes you fluent.

That's the whole SQLMaxx loop: 600+ real problems, instant AI feedback, mastery you can actually see, and spaced review that won't let you forget.

A stack of SQL practice problem cards, the top card showing an employees table.
615 problems · 66 concepts

Real problems. Not toy examples.

615 hand-built problems spanning all 66 concepts, from basic SELECTs to window functions, built on real schemas and real business questions, the kind you'll actually get asked on the job. Enough reps to make SQL automatic.

A retro computer showing a SQL query marked correct with a green checkmark.
Instant AI feedback

Write a query. Know if it's right in one second.

No copying an answer and hoping it clicked. The AI grader checks your real query against real data, catches exactly what's wrong, and explains the fix in plain English, like a senior analyst reading over your shoulder on every problem.

A circular mastery progress dial filling from blue to green, the SQLMaxx diamond at its center.
Mastery tracking

Stop guessing whether you actually know it.

SQLMaxx tracks every concept and shows you what you've mastered and what's still shaky. Your skills fill in one concept at a time, so 'I think I get joins' becomes something you can prove.

A SQL query editor circled by a blue return arrow with a clock, scheduled to come back for review.
Spaced review

Learn it once. Keep it for good.

Most of what you learn this week fades by next week. So when a concept comes due for review, SQLMaxx hands you a fresh problem to solve from a blank editor, not a flashcard to re-read. A research-backed spaced-repetition algorithm (FSRS) times each return for right before you'd forget, so your SQL is still there months later, when the interview or the job actually needs it.