Learning Goals
3 min- Use
COUNT,SUM,AVG,MIN,MAXfor whole-table aggregates. - Add
GROUP BY colto compute per-bucket aggregates. - Filter aggregated rows with
HAVING. - Combine groups with sorting and limiting for "top group" reports.
Warm-Up · One Row, Many Rows
5 minRun these on the students table from Lesson 17:
SELECT COUNT(*) FROM students; -- 8 SELECT AVG(score) FROM students; -- 77.5 SELECT MIN(age), MAX(age) FROM students; -- 13, 14
Each aggregate collapses many rows into one. GROUP BY changes "all rows" to "all rows per bucket".
Aggregates without GROUP BY give one row for the whole table. With GROUP BY col, you get one row per distinct value of col.
New Concept · The Five Aggregates + GROUP BY
14 minThe five
COUNT(*) number of rows COUNT(col) number of non-NULL values SUM(col) total AVG(col) average (mean) MIN(col), MAX(col)
Per-group counts
SELECT class, COUNT(*) AS num_students FROM students GROUP BY class;
class num_students Form 1A 4 Form 1B 4
Per-group averages
SELECT class, AVG(score) AS avg_score FROM students GROUP BY class;
class avg_score Form 1A 80.75 Form 1B 74.25
Multiple aggregates
SELECT class, COUNT(*) AS n, AVG(score) AS avg, MIN(score) AS lo, MAX(score) AS hi FROM students GROUP BY class;
Filter groups with HAVING
WHERE filters rows BEFORE grouping; HAVING filters groups AFTER:
-- Classes whose average is above 75 SELECT class, AVG(score) AS avg FROM students GROUP BY class HAVING AVG(score) > 75;
Common pitfall — naked columns
This query is illegal in standard SQL:
-- ✗ Wrong: 'name' isn't aggregated and isn't in GROUP BY SELECT class, name, AVG(score) FROM students GROUP BY class;
Every column in SELECT must either be in GROUP BY or be an aggregate. SQLite is lenient and runs it but the result is unpredictable. Don't rely on it.
Worked Example · Class Report Card
12 minSELECT class, COUNT(*) AS students, ROUND(AVG(score), 1) AS avg_score, MIN(score) AS lowest, MAX(score) AS highest, COUNT(CASE WHEN score >= 80 THEN 1 END) AS num_distinctions FROM students GROUP BY class ORDER BY avg_score DESC;
class students avg_score lowest highest num_distinctions Form 1A 4 80.8 65 95 2 Form 1B 4 74.2 55 92 2
Read the diff
Two tricks worth knowing:
ROUND(x, 1)rounds the average to one decimal — readable reports.COUNT(CASE WHEN condition THEN 1 END)counts only rows that match — the SQL idiom for "how many distinctions".
Try It Yourself
13 minReturn one number: how many students are in the table.
Hint
SELECT COUNT(*) FROM students;
One row per age, columns: age, n students, average score.
Hint
SELECT age, COUNT(*) AS n, AVG(score) AS avg FROM students GROUP BY age;
A "fail" is score < 60. Find classes that have at least one failing student.
Hint
SELECT class FROM students WHERE score < 60 GROUP BY class HAVING COUNT(*) >= 1; -- (or just SELECT DISTINCT class FROM students WHERE score < 60;)
Mini-Challenge · Pizza Sales Summary
8 minCreate a pizza_sales table with columns id, branch, product, quantity, price, date. Insert ~10 rows across 3 branches. Write one query that returns per branch: number of orders, total revenue, average order value, and the date of the most recent order.
Show one possible solution
SELECT branch, COUNT(*) AS orders, SUM(quantity * price) AS revenue, ROUND(AVG(quantity * price), 2) AS avg_order, MAX(date) AS last_sale FROM pizza_sales GROUP BY branch ORDER BY revenue DESC;
Recap
3 minFive aggregate functions; one GROUP BY clause; HAVING filters AFTER. The rule of thumb: every non-aggregate column in SELECT must appear in GROUP BY. Combining groups + sorting + limiting gives you "top group", "biggest customer", "worst-performing day" — the building blocks of every dashboard.
Homework
4 minRe-load the sales.csv from earlier lessons as a SQL table. Write 5 aggregate queries:
- Total transactions.
- Revenue per product.
- Day with the highest revenue.
- Products that sold more than 5 units total.
- Average sale value per product, descending.
SELECT COUNT(*) FROM sales; SELECT product, SUM(quantity * price) AS revenue FROM sales GROUP BY product ORDER BY revenue DESC; SELECT date, SUM(quantity * price) AS rev FROM sales GROUP BY date ORDER BY rev DESC LIMIT 1; SELECT product, SUM(quantity) AS units FROM sales GROUP BY product HAVING units > 5; SELECT product, ROUND(AVG(quantity * price), 2) AS avg FROM sales GROUP BY product ORDER BY avg DESC;