GROUP BY Basics: Aggregating Rows Into Summaries
Collapse individual orders into one summary row per customer using GROUP BY, COUNT, and SUM.
Overview
GROUP BY collapses many rows that share a common value into a single
summary row, so you can pair it with aggregate functions like COUNT and
SUM to answer questions like "how many orders has each customer placed,
and how many total items did they buy?" Instead of scanning a raw orders
table row by row, you get one row per customer with the numbers already
rolled up.
This is the foundation of almost every reporting query: totals per customer,
counts per category, revenue per day, and so on all start from the same
GROUP BY + aggregate pattern.
The Query
Loading database engine...
Step-by-Step Breakdown
FROM orders— start from the raw, one-row-per-order table.GROUP BY customer_id— bucket all rows into groups that share the samecustomer_id. Every customer with at least one order gets exactly one group.COUNT(*)— within each group, count how many order rows fell into it — the customer's total number of orders.SUM(quantity)— within each group, add up thequantitycolumn across all of that customer's orders.ORDER BY order_count DESC, customer_id— sort the summarized rows, not the original ones. Once youGROUP BY,ORDER BYoperates on the aggregated result set.
Notice that SELECT only lists customer_id and aggregate expressions.
Every column that isn't wrapped in an aggregate function must appear in the
GROUP BY clause — that's the core rule to internalize.
Variations
Add a HAVING clause to keep only customers with more than 2 orders:
Loading database engine...
HAVING filters groups after they've been aggregated, which is why it can
reference COUNT(*) directly. WHERE can't do this — it only sees
individual rows, before grouping happens, so it has no concept of a group's
total count yet.
WHERE runs before GROUP BY, HAVING runs after
If you wanted to only count completed orders per customer, you'd add
WHERE status = 'completed' — that filters rows before grouping. If you
wanted to only show customers whose completed-order count exceeds some
number, that's a job for HAVING on the aggregate, since the aggregate
doesn't exist yet at the time WHERE runs.
Common Mistakes
- Selecting a non-aggregated, non-grouped column.
SELECT customer_id, order_date, COUNT(*) ... GROUP BY customer_idfails (or gives an arbitrary value) in most databases becauseorder_dateisn't unique per group and isn't in theGROUP BYlist. - Using
WHEREwhen you meantHAVING.WHERE COUNT(*) > 2is invalid SQL — aggregates aren't available until after grouping, which is exactly whatHAVINGexists for. - Forgetting
COUNT(*)counts rows, not distinct values. If a customer could have duplicate order rows for some reason,COUNT(*)still counts each one; useCOUNT(DISTINCT column)when you specifically need unique values. - Grouping by the wrong granularity. Grouping by
customer_idalone loses per-product detail; if you need per-customer-per-product totals, you'dGROUP BY customer_id, product_idinstead.