SQL · Week 3 of 4 · 5–6 hours
Aggregate Functions, GROUP BY and HAVING
Turn many rows into summary numbers, overall and per group.
Learning objectives
By the end of this week you will be able to:
- Use COUNT, SUM, AVG, MIN and MAX.
- Summarize per category with GROUP BY.
- Filter grouped results with HAVING.
- Explain the difference between WHERE and HAVING.
Lessons
Aggregate Functions
An aggregate function takes many rows and returns one number.
| Function | Returns | Example |
|---|---|---|
| COUNT(*) | number of rows | SELECT COUNT(*) FROM orders; |
| COUNT(column) | rows where that column is not NULL | COUNT(city) |
| COUNT(DISTINCT column) | number of different values | COUNT(DISTINCT customer_id) |
| SUM | total | SUM(quantity) |
| AVG | average | AVG(price) |
| MIN / MAX | smallest / largest | MAX(order_date) = most recent order |
You can combine several in one query and name them: SELECT COUNT(*) AS orders, SUM(quantity) AS units FROM orders;
Good to know: aggregates ignore NULL values. AVG of 10, NULL and 20 is 15, not 10.
GROUP BY
Aggregates on their own give one number for the whole table. GROUP BY splits the rows into groups first, then calculates the aggregate for each group, just like a PivotTable.
SELECT category, COUNT(*) AS products, AVG(price) AS avg_price FROM products GROUP BY category;
This returns one row per category with its product count and average price.
The golden rule: every column in SELECT must either be in the GROUP BY or be inside an aggregate function. SELECT category, product_name, COUNT(*) ... GROUP BY category fails (or gives misleading results) because SQL cannot show one product name for a whole group.
You can group by more than one column: GROUP BY category, city gives one row for each combination.
HAVING Clause
HAVING filters groups after they are summarized. WHERE filters rows before they are grouped.
SELECT customer_id, COUNT(*) AS order_count FROM orders WHERE order_date >= '2026-01-01' -- rows: only 2026 orders GROUP BY customer_id HAVING COUNT(*) >= 3 -- groups: only customers with 3+ orders ORDER BY order_count DESC;
How SQL runs it: FROM picks the table, WHERE removes rows, GROUP BY forms groups, HAVING removes groups, SELECT picks the output, ORDER BY sorts it. That is why you cannot use an aggregate such as COUNT(*) in WHERE: the groups do not exist yet.
Watch
Practice
- How many orders are there in total?
- How many different customers have placed an order?
- What is the average, cheapest and most expensive product price?
- How many products are in each category?
- What is the total quantity sold for each product_id?
- Which categories have an average price above 30?
- Which customers placed more than two orders in 2026?
Check your understanding
Try each question first, then open it to see the answer.
What is the difference between COUNT(*) and COUNT(city)?
COUNT(*) counts all rows; COUNT(city) skips rows where city is NULL.
Why does SELECT category, product_name, COUNT(*) FROM products GROUP BY category cause a problem?
product_name is neither grouped nor aggregated, so SQL cannot show one value per group.
You want only groups with total sales over 1,000. WHERE or HAVING?
HAVING, because the total exists only after grouping.
Assignment
Submit your seven queries with comments, and a short paragraph describing what the results tell you about the store.