Aggregation and GROUP BY
Hookโ
The merchandising manager stops by your desk: "Forget the individual orders โ just tell me how much revenue each product category brought in last month, and which ones cleared our $100 reporting threshold." You already know how to pull rows. But the manager does not want rows; they want one number per category. Handing over three thousand order lines would be worse than useless. You need to collapse those rows into a summary.
Conceptโ
Filtering and sorting return the rows you started with, just fewer of them. Aggregation is different: it collapses many rows into a single summary value. An aggregate function takes a whole column and returns one number.
| Function | Answers |
|---|---|
COUNT(*) | How many rows? |
SUM(amount) | What is the total? |
AVG(amount) | What is the average? |
MIN / MAX | What is the smallest / largest? |
On their own, these squash the entire table into one row: SELECT SUM(amount) FROM orders gives you total revenue across everything. That is rarely the
question. The manager wants revenue per category โ one summary for each
group. That is what GROUP BY does: it splits the rows into groups by a
column's value, then runs the aggregate once per group.
SELECT category, SUM(amount) AS revenue
FROM orders
GROUP BY category;
The mental model: GROUP BY category sorts every row into buckets โ one bucket
per distinct category โ and SUM(amount) totals each bucket separately. The
result has one row per bucket. This gives you the rule that trips up everyone
at first: every column in your SELECT must either be inside an aggregate
function or named in GROUP BY. If you group by category and also select
order_id, the database cannot answer "which order id?" for a bucket of two
hundred orders โ so it refuses.
Two more pieces complete the picture. AS revenue renames the output column to
something a reader understands โ an alias. And when you need to filter on
the summary itself ("only categories over $100"), you cannot use WHERE,
because WHERE ran before the groups existed. You use HAVING, which filters
groups after aggregation:
SELECT category, SUM(amount) AS revenue
FROM orders
GROUP BY category
HAVING SUM(amount) > 100;
The distinction is worth memorizing because it is asked in every SQL interview:
WHERE filters rows before grouping; HAVING filters groups after.
Worked exampleโ
Let me answer the merchandising manager's question exactly as asked: revenue
and order count per category, showing only the categories that cleared the $100
threshold. There is one wrinkle a careful analyst catches โ cancelled orders
never brought in money, so they must be excluded before totalling. That
exclusion is a row filter, so it belongs in WHERE, before grouping.
So I have two filters doing two different jobs. WHERE status <> 'cancelled'
throws out cancelled rows first (<> means "not equal"). Then GROUP BY category buckets what remains. Then HAVING SUM(amount) > 100 keeps only the
buckets over threshold. I sort by revenue so the biggest category leads, with
category name as a tiebreaker so the order is stable.
SELECT category,
COUNT(*) AS order_count,
ROUND(SUM(amount), 2) AS revenue
FROM orders
WHERE status <> 'cancelled'
GROUP BY category
HAVING SUM(amount) > 100
ORDER BY revenue DESC, category;
category order_count revenue
electronics 3 410.0
home 2 410.0
The books category is gone from the result โ not because of an error, but
because its non-cancelled revenue was only 100 threshold, so
HAVING filtered it out. Note the honest detail: electronics and home tie at
410 โ and books fell short at $55."
Hands-onโ
Your turn. The exercise below loads the same store's orders table for a
different stakeholder. The finance analyst wants the average order value for
each order status (pending, shipped, cancelled), rounded to two decimals and
listed highest average first, so they can see whether shipped orders skew
larger than pending ones. Write the aggregation query.
Success criteria: one row per status, an avg_value column rounded to two
decimals, sorted descending by that average. SQL Kingdom validates your grouped
result and awards XP on a match.
Recapโ
- You can collapse many rows into decision-ready numbers with
COUNT,SUM,AVG,MIN, andMAX. - You can produce one summary per group with
GROUP BY, and you know every selected column must be aggregated or grouped. - You can filter on the summary itself with
HAVING, and you can explain why that is different fromWHERE.
Next up: every summary so far came from one table. Real questions span two โ "revenue by region" needs orders joined to customers. That is joins for analysts.
- Aggregate functions (
COUNT,SUM,AVG,MIN,MAX) collapse a column into one number. GROUP BYproduces one summary row per group; every selected column is aggregated or grouped.WHEREfilters rows before grouping;HAVINGfilters group summaries after.