From rows to answers
Sessions b1 and b2 gave you rows - filtered, sorted, but still one line per record. Leadership rarely wants 33 order rows; they want "how many orders?" and "how much revenue?". Aggregate functions squeeze many rows into a single number, and GROUP BY repeats that squeeze once per group so you get "orders per country" or "average price per category". That is the leap from data to insight.
COUNT, SUM, AVG, MIN, MAX), GROUP BY to compute them per group, HAVING to filter those groups, and DISTINCT to count unique values. You will finish by computing revenue per product on the real order data.
Aggregate functions 7 min live
An aggregate function takes a whole column of values and returns one number. COUNT tallies rows, SUM adds them up, AVG averages, MIN and MAX find the extremes. Applied to a whole table with no grouping, they collapse every row into a single summary line - the simplest form of a report.
| Function | Returns | Example question |
|---|---|---|
| COUNT(*) | how many rows | "How many orders do we have?" |
| SUM(x) | total of a column | "What is total revenue?" |
| AVG(x) | the mean of a column | "What is our average product price?" |
| MIN(x) / MAX(x) | the smallest / largest value | "Cheapest and priciest product?" |
LiveCOUNT - how many rows2 min▶
COUNT(*) returns the number of rows, collapsing the whole table to one number. The AS orders renames the output column so the result reads cleanly. Press ▶ Run, then change orders to customers or products.
SELECT COUNT(*) AS orders FROM orders;
GROUP BY yet means "the whole table is one group".
LiveAVG - the average, rounded3 min▶
AVG(price) averages the price column. Raw averages spill long decimals, so wrap it in ROUND(..., 2) to keep two places - a small habit that makes reports readable. Change the 2 to 0 to round to whole dollars.
SELECT ROUND(AVG(price),2) AS avg_price FROM products;
LiveMIN and MAX together2 min▶
You can compute several aggregates in one query - here the cheapest and priciest product side by side. Both scan the same price column and each returns one value, so the result is a single tidy row.
SELECT MIN(price) AS cheapest, MAX(price) AS priciest FROM products;
The "one big number" dashboard tile. Every KPI card you have ever seen - total revenue, active users, average order value - is one aggregate function under the hood. When a stakeholder asks "just give me the headline number", they are asking for a SUM, a COUNT, or an AVG. You already know how to write all three.
GROUP BY - per what? 7 min live
One number for the whole table is a start, but the real questions are "per country", "per category", "per month". GROUP BY sorts rows into buckets by a column, then runs the aggregate once per bucket. The rule of thumb: whatever you put after GROUP BY is the "per what" of your question.
LiveCount customers per country3 min▶
This is the diagram, live. Group by country, count the rows in each group, and sort so the biggest markets sit on top. The output has one row per country - exactly what "per country" promised.
SELECT country, COUNT(*) AS customers FROM customers GROUP BY country ORDER BY customers DESC;
SELECT that is not inside an aggregate must appear in GROUP BY. Here country is grouped and COUNT(*) is aggregated - balanced. Break the rule and other databases error; SQLite guesses, which is worse.
LiveAverage price per category3 min▶
Swap COUNT for AVG and you get the mean price within each product category - one line for Coffee, one for Equipment, one for Add-on. This single query would take a pivot table and several clicks in a spreadsheet.
SELECT category, ROUND(AVG(price),2) AS avg_price FROM products GROUP BY category;
HAVING and DISTINCT 6 min live
Two finishing tools. HAVING filters groups after aggregation - the counterpart to WHERE, which filters rows before. And DISTINCT strips duplicates, which is how you count unique things: how many different countries, not how many customers.
| Clause | Filters... | Runs... |
|---|---|---|
| WHERE | individual rows | before grouping - "only USA customers" |
| HAVING | whole groups | after grouping - "only groups with 3+ members" |
LiveHAVING - filter groups after aggregating3 min▶
You cannot put an aggregate in WHERE - the counts do not exist yet when WHERE runs. HAVING comes after grouping, so it can. This keeps only the plans with three or more customers. Change 3 to 10 and watch groups drop out.
SELECT plan, COUNT(*) AS n FROM customers GROUP BY plan HAVING COUNT(*) >= 3;
"Show me products that sold more than 100 units." That is GROUP BY product then HAVING SUM(quantity) > 100. Any request of the form "groups that meet a threshold" - high-volume SKUs, cities with many complaints, reps over quota - is a HAVING clause. Mixing it up with WHERE is the most common aggregation bug, so the order matters: filter rows with WHERE, filter groups with HAVING.
LiveDISTINCT - count the unique values3 min▶
COUNT(DISTINCT country) counts how many different countries appear, ignoring repeats. Plain COUNT(country) would count every customer; the DISTINCT is what turns it into "how many markets do we operate in".
SELECT COUNT(DISTINCT country) AS countries FROM customers;
Revenue per product, from the line items ★ 14 min · everyone builds
Daybreak's founder asks the big one: "Which products make us the most money?" Revenue lives in order_items, where each line is a quantity times a unit_price. You have not learned joins yet (that is b4), so we will stay inside one table and aggregate by product_id. It is the most useful query in the whole session.
See the raw material: SELECT * FROM order_items LIMIT 8; - each row is one product line, with quantity and unit_price.
Compute line revenue: quantity * unit_price. That is money for a single line.
Total it per product: SUM(quantity * unit_price) with GROUP BY product_id - one revenue figure per product.
Rank it: ORDER BY revenue DESC puts the top earners first. That is the founder's answer.
SELECT product_id, SUM(quantity*unit_price) AS revenue FROM order_items GROUP BY product_id ORDER BY revenue DESC;
This is the revenue report. "Top products by revenue" is one of the most-run queries in any commerce business, and you just wrote it in four lines. The only thing missing is the product name instead of the id - and that is a single join away in session b4. The hard part, the aggregation logic, is already done.
Three questions, your queries ★ 10 min · build your own
No scaffolding now. Each editor answers one aggregation question - run it, tweak it, break it, fix it. The run-fix-run loop is the whole skill.
LiveQ1 · How many orders came through each channel?3 min▶
Group the orders by channel (web vs app) and count each bucket. One row per channel comes back.
SELECT channel, COUNT(*) AS orders FROM orders GROUP BY channel ORDER BY orders DESC;
LiveQ2 · How many products are in each category?3 min▶
Group products by category and count. This tells you where the catalog is deep and where it is thin.
SELECT category, COUNT(*) AS products FROM products GROUP BY category ORDER BY products DESC;
Self-studyQ3 · How many distinct customers placed orders?2 min▶
The orders table has 33 rows but fewer distinct customers - some people ordered more than once. COUNT(DISTINCT customer_id) counts the unique buyers.
SELECT COUNT(DISTINCT customer_id) AS buyers FROM orders;
Try it yourself - this week ◐ 20-30 min total
- Count how many orders fall into each
status(completed, refunded, cancelled). Which dominates? - Find the total quantity sold per product with
SUM(quantity)andGROUP BY product_id, biggest first. - Using
HAVING, list only the countries with two or more customers. - Compute the average
monthly_qtyacross the subscriptions table withAVG. - Bring one "per group" question about Daybreak that also needs a product or customer name to session b4 - that is exactly the gap joins fill.
Official sources covered
This session teaches the aggregation core of the major free SQL curricula, run on live Daybreak data instead of screenshots. Certificates and graded problem sets stay on the official sites (linked below). This page covers:
Three questions before you go 🎯 ◐ 90 seconds
1 · What does GROUP BY do?
GROUP BY buckets rows by a column and returns one row per bucket, letting COUNT/SUM/AVG run once per group. Sorting is ORDER BY; de-duping is DISTINCT.
2 · What is the difference between WHERE and HAVING?
WHERE runs first on individual rows; HAVING runs after grouping and can test aggregates like COUNT(*). That order is why HAVING can see counts and WHERE cannot.
3 · What does COUNT(DISTINCT country) return?
DISTINCT strips duplicates first, so COUNT(DISTINCT country) counts how many different countries appear - the number of unique values, not total rows.