GROUP BY: a report by groups
In short: GROUP BY puts rows with the same value into one group, so that COUNT, SUM or AVG are calculated for each group separately.
Problem
The warehouse dashboard
The warehouse wants a dashboard: how many orders are new, how many paid, how many shipped. With COUNT(*) and WHERE status = 'new' you would need five queries, one for each status. And if a sixth status appears tomorrow, the report will not show it.
You need one query that finds the statuses by itself and counts each one.
The idea
GROUP BY
GROUP BY column puts the rows with the same value in the column into one group. The aggregate function is worked out for each group separately:
60 orders, five groups, five rows. Each group gives one row.
The customers by city:
Boris has no city and is in a separate group with city = NULL. For GROUP BY, as for DISTINCT, all NULLs are one group.
How one group becomes one row is easier to see on a few rows. The last three orders are six rows in order_items:
SELECT order_id, COUNT(*) AS lines, ROUND(SUM(quantity * unit_price), 2) AS totalFROM order_itemsWHERE order_id >= 58GROUP BY order_id;Step by step, in words
- 6 rows from order_items: the products in orders 58, 59 and 60.
- GROUP BY order_id: every row goes into the group of its order_id.
- 3 different order_id values, 3 groups. Each has 2 rows.
- The group of 58 becomes one row: COUNT(*) is 2, and SUM(quantity * unit_price) is 1 × 1.20 + 4 × 1.90 = 8.80 €.
- The group of 59: 2 rows, 3 × 2.50 + 2 × 3.20 = 13.90 €.
- The group of 60: 2 rows, 2 × 1.20 + 4 × 2.90 = 14.00 €.
- 6 rows went in, 3 came out: one for each group.
The query from the scene. ROUND(…, 2) rounds the sum to the cent:
Try it
Orders per customer
How many orders has each customer placed? Show customer_id and the count.
Solution
SELECT customer_id, COUNT(*)
FROM orders
GROUP BY customer_id;SELECT customer_id, COUNT(*) FROM orders GROUP BY customer_id. 18 rows: two of the twenty customers have no orders, so they are not there. Customer 1 has 12 orders, the most of all.
The idea
The value of each order
In order_items an order takes several rows, one for each product. A group by order_id brings together the lines of one order:
Now each order is one row: how many products it has and how much it is worth.
A group can also be by two columns. Then one group holds the rows where both values are the same. The orders of customer 1 by status:
A group can also be by a computed value. strftime('%Y-%m', ordered_at) takes the year and month from the date, and the orders are grouped by month:
See it
The column that is not in the group
The orders per customer, and next to them the status. Looks reasonable:
The orders of customer 8 show what is going on:
Three orders, three different statuses. The group is one row, and it has room for one value. SQLite says nothing and takes the status from one of the rows. Which one is not promised.
The rule: every column in SELECT is either in GROUP BY or inside an aggregate (COUNT, SUM, MIN…). PostgreSQL and SQL Server give an error for such a query, and so does MySQL with its default settings since version 5.7. SQLite accepts it and shows something that looks like an answer.
If you want the statuses, group by both columns: GROUP BY customer_id, status.
More precisely, if you are curious
SQLite has one quirk that is written in its documentation: if the query has exactly one MIN or MAX, the free column comes from the row with the smallest or largest value. SELECT customer_id, MAX(ordered_at), id FROM orders GROUP BY customer_id gives the number of the latest order. This is SQLite only. Other databases refuse it, and in the subqueries module we will see how to do it everywhere.
Try it
The best sellers
Which five products have sold the most items? Show product_id and the items in a column called sold, the best seller first.
Solution
SELECT product_id, SUM(quantity) AS sold
FROM order_items
GROUP BY product_id
ORDER BY sold DESC
LIMIT 5;GROUP BY product_id, SUM(quantity) AS sold, then ORDER BY sold DESC LIMIT 5. First is product 13 with 31 items: the cucumbers, which have run out. Maybe it is time for a new delivery.
Exercise
Two reports
1. How many products and how many items in stock are there in each category?
Show category_id, the number of products and the sum of their stock.
Solution
SELECT category_id, COUNT(*), SUM(stock)
FROM products
GROUP BY category_id;SELECT category_id, COUNT(*), SUM(stock) FROM products GROUP BY category_id. Seven groups: six categories and NULL for the gift card. Its 999 cards are not goods on a shelf, so in a real report we would drop it with WHERE category_id IS NOT NULL.
2. Accounting wants the number of orders for each month of 2026, sorted by month.
Show the month (year-month) and the number of orders for each month of 2026, in month order.
Solution
SELECT strftime('%Y-%m', ordered_at) AS month, COUNT(*)
FROM orders
WHERE ordered_at >= '2026-01-01'
GROUP BY strftime('%Y-%m', ordered_at)
ORDER BY month;WHERE ordered_at >= '2026-01-01' keeps the orders from 2026, GROUP BY strftime('%Y-%m', ordered_at) splits them by month, and ORDER BY month sorts them. Ten months, from January to October. Why ORDER BY can use the alias month, we will see in the last lesson of the module.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: ORDER BY, LIMIT and DISTINCT · Arithmetic in a query · Date functions · Aggregate functions: COUNT, SUM, AVG
- Continues in: HAVING: a condition for the groups · The order of query execution · LEFT JOIN: rows without a match too · Many tables, and a table joined to itself
- Tasks on this topic: Summarising