All 40 lessons

Lesson 15 of 40 · Aggregation

SQL HAVING Clause

By Skillancy Editorial Team

Every example runs on the ShopKart dataset, already loaded in the free SQL Compiler. Click Try it in the SQL Compiler under a query to open it there, then press Run.

HAVING filters groups after GROUP BY has calculated them. Use it whenever the condition depends on an aggregate, such as "customers with more than 2 orders" or "products that sold over ₹20,000".

Syntax

SELECT group_column, AGGREGATE(col)
FROM table_name
WHERE row_condition        -- filters rows
GROUP BY group_column
HAVING group_condition     -- filters groups
ORDER BY ...;

WHERE vs HAVING

WHEREHAVING
Filtersindividual rowsgroups
Runsbefore GROUP BYafter GROUP BY
Can use aggregates?noyes
ExampleWHERE status = 'Delivered'HAVING COUNT(*) > 2

Examples

1. Repeat customers

Customers who placed 3 or more orders:

SELECT customer_id,
       COUNT(*) AS orders
FROM orders
GROUP BY customer_id
HAVING COUNT(*) >= 3
ORDER BY orders DESC;
Try it in the SQL Compiler

Result:

customer_idorders
14
33
43

(3 rows)

2. Best-selling products by revenue

Products with more than ₹20,000 in net revenue:

SELECT product_id,
       ROUND(SUM(quantity * unit_price * (1 - discount_pct / 100)), 2) AS net_revenue
FROM order_items
GROUP BY product_id
HAVING SUM(quantity * unit_price * (1 - discount_pct / 100)) > 20000
ORDER BY net_revenue DESC;
Try it in the SQL Compiler

Result:

product_idnet_revenue
10168397.15
10355982.50
10524222.15

(3 rows)

Notice the full expression is repeated in HAVING. PostgreSQL won't accept the alias net_revenue there.

3. WHERE and HAVING together

Customers with at least 2 delivered orders:

SELECT customer_id,
       COUNT(*) AS delivered_orders
FROM orders
WHERE status = 'Delivered'       -- step 1: keep delivered rows
GROUP BY customer_id             -- step 2: group by customer
HAVING COUNT(*) >= 2             -- step 3: keep groups with 2+
ORDER BY delivered_orders DESC, customer_id;
Try it in the SQL Compiler

Result:

customer_iddelivered_orders
13
33
22
42

(4 rows)

Compare with the same query without WHERE: customers would qualify based on cancelled and pending orders too.

4. Groups with an average condition

Categories whose average product price is over ₹3,000:

SELECT category_id,
       COUNT(*) AS products,
       ROUND(AVG(price), 0) AS avg_price
FROM products
GROUP BY category_id
HAVING AVG(price) > 3000
ORDER BY avg_price DESC;
Try it in the SQL Compiler

Result:

category_idproductsavg_price
1319096
234299

(2 rows)

5. Finding duplicates

HAVING COUNT(*) > 1 is the standard way to find duplicate values, a very common interview question:

SELECT city, COUNT(*) AS customers
FROM customers
GROUP BY city
HAVING COUNT(*) > 1;
Try it in the SQL Compiler

Result:

citycustomers
Bengaluru2

(1 row)

Common mistakes

Putting an aggregate in WHERE:

SELECT customer_id, COUNT(*)
FROM orders
WHERE COUNT(*) >= 3
GROUP BY customer_id;
Try it in the SQL Compiler

Error: aggregate functions are not allowed in WHERE

  • Using HAVING for row filters. HAVING status = 'Delivered' (with status grouped) works but is slower and confusing. Row conditions belong in WHERE.
  • Using the alias in HAVING in PostgreSQL. Repeat the expression.

Practice question

Which payment methods were used for more than 5 orders? Show the method and the number of orders.

Show solution
SELECT payment_method,
       COUNT(*) AS orders
FROM orders
GROUP BY payment_method
HAVING COUNT(*) > 5
ORDER BY orders DESC;
Try it in the SQL Compiler

Result:

payment_methodorders
UPI11
Card7

(2 rows)

Key takeaways

  • HAVING filters groups; WHERE filters rows.
  • Conditions on COUNT, SUM, AVG etc. go in HAVING.
  • Query order: WHERE → GROUP BY → HAVING → SELECT → ORDER BY.
  • GROUP BY ... HAVING COUNT(*) > 1 finds duplicates.

Frequently asked questions

What is the difference between WHERE and HAVING?

WHERE filters rows before they are grouped and cannot use aggregates. HAVING filters groups after GROUP BY and is where conditions like COUNT(*) > 2 belong.

Can I use HAVING without GROUP BY?

Yes. The whole table is treated as one group, so HAVING decides whether that single summary row is returned. It is rarely needed in practice.

Can HAVING use a column alias?

Not in standard SQL or PostgreSQL, because HAVING is evaluated before SELECT. Repeat the aggregate expression. MySQL does allow aliases in HAVING.

Newsletter

Keep learning SQL

One email a week with SQL and analytics guides, new practice questions and cohort dates. No spam, unsubscribe anytime.

Prefer a one-page summary? Download the free SQL cheat sheet (PDF)