5 min read
GROUP BY and HAVING in SQL, with examples
GROUP BY collapses rows that share a value into one row per group. Aggregate functions such as COUNT, SUM, AVG, MIN and MAX then summarise each group.
One row per group
How many customers are there in each country?
SELECT country, COUNT(*) AS customers FROM customers GROUP BY country ORDER BY customers DESC, country;
| country | customers |
|---|---|
| United States | 12 |
| United Kingdom | 9 |
| Portugal | 8 |
| Canada | 7 |
| Kenya | 6 |
| Australia | 3 |
| India | 3 |
Every column in the SELECT list must either be in the GROUP BY or be inside an aggregate function.
WHERE filters rows, HAVING filters groups
WHERE runs before grouping, so it decides which rows go into the groups. HAVING runs after, so it can filter on the aggregate itself. This finds customers who placed more than five orders.
SELECT c.email, COUNT(*) AS order_count FROM customers c JOIN orders o ON o.customer_id = c.customer_id GROUP BY c.customer_id HAVING COUNT(*) > 5 ORDER BY order_count DESC, c.email;
| order_count | |
|---|---|
| chloe.martin@example.com | 13 |
| liam.murphy@example.com | 11 |
| thandi.ndlovu@example.com | 10 |
| wes.hughes@example.com | 8 |
| amara.okafor@example.com | 7 |
| zoe.walsh@example.com | 7 |
| diego.alves@example.com | 6 |
| esther.mensah@example.com | 6 |
Showing 8 of 9 rows.
Using both together
Ignore cancelled orders with WHERE, then keep only months with more than 3,000 in revenue with HAVING.
SELECT strftime('%Y-%m', o.order_date) AS month,
ROUND(SUM(oi.quantity * oi.unit_price), 2) AS revenue
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
WHERE o.status <> 'cancelled'
GROUP BY month
HAVING revenue > 3000
ORDER BY month;| month | revenue |
|---|---|
| 2025-10 | 4745.5 |
| 2025-11 | 6871.4 |
| 2026-01 | 5060.8 |
| 2026-02 | 3311.1 |
| 2026-06 | 7446.8 |
| 2026-07 | 9640.4 |
| 2026-09 | 8174.3 |
Order of evaluation
SQL processes a query in this order: FROM and joins → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT. Knowing this explains why you can't use an aggregate in WHERE.
Reading only gets you so far. Practise it now, with instant feedback.
Try a HAVING challenge