Data Analyst Program

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.

FunctionReturnsExample
COUNT(*)number of rowsSELECT COUNT(*) FROM orders;
COUNT(column)rows where that column is not NULLCOUNT(city)
COUNT(DISTINCT column)number of different valuesCOUNT(DISTINCT customer_id)
SUMtotalSUM(quantity)
AVGaverageAVG(price)
MIN / MAXsmallest / largestMAX(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

  1. How many orders are there in total?
  2. How many different customers have placed an order?
  3. What is the average, cheapest and most expensive product price?
  4. How many products are in each category?
  5. What is the total quantity sold for each product_id?
  6. Which categories have an average price above 30?
  7. 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.