How to Use HAVING in SQL

Use HAVING when you need to filter grouped results after aggregation. It works like WHERE, but instead of filtering individual rows, it filters the summary groups created by GROUP BY.

What you’ll build or solve

You’ll learn how to use HAVING in SQL to filter aggregated groups. You’ll also know when to use WHERE first and HAVING after grouping.

When this approach works best

This approach is the right choice when grouped totals, averages, or counts should meet a condition.

Common real-world scenarios include:

  • Countries with 100+ users
  • Products with average rating above 4
  • Plans with more than 50 signups
  • Classes with low pass rates
  • Categories with total revenue thresholds

This is a bad idea when the filter applies to individual rows before grouping. In that case, use WHERE.

Prerequisites

You only need:

  • A SQL table with rows
  • Basic GROUP BY and aggregate knowledge

Step-by-step instructions

Step 1: Filter grouped results after GROUP BY

Group the rows first.

SELECT country, COUNT(*)
FROM users
GROUP BY country
HAVING COUNT(*) > 100;

This returns only countries with more than 100 users.

This is different from WHERE, which filters rows before grouping.

You can combine both.

SELECT status, SUM(total)
FROM orders
WHERE created_at >= '2026-01-01'
GROUP BY status
HAVING SUM(total) > 10000;

What to look for:

  • HAVING filters groups, not raw rows
  • Use it after GROUP BY
  • Great for count and revenue thresholds
  • WHERE still runs before grouping
  • Works with all aggregate functions

Examples you can copy

Popular plans

SELECT plan, COUNT(*)
FROM users
GROUP BY plan
HAVING COUNT(*) > 50;

High-revenue countries

SELECT country, SUM(total)
FROM orders
GROUP BY country
HAVING SUM(total) > 5000;

Top-rated products

SELECT product_id, AVG(rating)
FROM reviews
GROUP BY product_id
HAVING AVG(rating) >= 4.5;

Common mistakes and how to fix them

Mistake 1: Using HAVING instead of WHERE for row filters

What the reader might do:

SELECT status, COUNT(*)
FROM orders
GROUP BY status
HAVING created_at >= '2026-01-01';

Why it breaks: created_at is a row-level filter.

Corrected approach:

Use WHERE before grouping.

Mistake 2: Forgetting GROUP BY

What the reader might do:

SELECT COUNT(*)
FROM users
HAVING COUNT(*) > 100;

Why it breaks: some databases allow this, but the intent is less clear.

Corrected approach:

Use GROUP BY when filtering grouped dimensions.

Mistake 3: Overusing aggregates in both SELECT and HAVING

What the reader might do:

HAVING SUM(total) > 100
AND AVG(total) > 50

Why it breaks: this may be correct, but can become hard to reason about.

Corrected approach:

Use aliases where supported or keep threshold logic focused.

Troubleshooting

If the filter should apply before grouping, switch to WHERE.

If SQL errors, confirm aggregates are valid in HAVING.

If too many groups appear, tighten the threshold.

If the grouped dimension is missing, add GROUP BY.

Quick recap

  • Use HAVING to filter grouped results
  • Place it after GROUP BY
  • Use WHERE for row-level filters
  • Great for count and revenue thresholds
  • Works with aggregate functions