Grouping rows with GROUP BY
The last lesson produced one figure for a whole table. GROUP BY produces one figure for each group: the number of products in each category, the number of customers in each city, the total salary of each department. It is the heart of every report.
The idea
GROUP BY sorts the rows into piles, one pile for each different value of a column. Then the aggregate functions run once for each pile, and each pile becomes one row of the result.
SELECT category, COUNT(*) AS products
FROM products
GROUP BY category;Result
| category | products |
|---|---|
| Electronics | 4 |
| Grocery | 4 |
| Home | 4 |
| Stationery | 4 |
4 rows
There are four categories, so there are four rows. Each shows the category and how many products are in its pile.
Any aggregate works
SELECT category,
COUNT(*) AS products,
MIN(price) AS cheapest,
MAX(price) AS dearest,
ROUND(AVG(price)) AS average
FROM products
GROUP BY category;Result
| category | products | cheapest | dearest | average |
|---|---|---|---|---|
| Electronics | 4 | 110 | 1299 | 577.0 |
| Grocery | 4 | 145 | 650 | 309.0 |
| Home | 4 | 349 | 1450 | 787.0 |
| Stationery | 4 | 60 | 750 | 255.0 |
4 rows
SELECT department, SUM(salary) AS monthly_cost
FROM employees
GROUP BY department;Result
| department | monthly_cost |
|---|---|
| Management | 95000 |
| Sales | 156000 |
| Support | 69000 |
| Warehouse | 72000 |
4 rows
The one big rule
Every column in the SELECT list must be either:
- named in the
GROUP BY, or - inside an aggregate function.
The reason is the same as in the last lesson. Each group becomes one row. A column that is the same for the whole group, because you grouped by it, has one value to show. A column that varies inside the group does not, unless you say how to boil it down.
SELECT category, name, COUNT(*)
FROM products
GROUP BY category;Which of the four Grocery product names should appear on the one Grocery row? PostgreSQL, SQL Server and Oracle stop with an error. SQLite picks a name from some row of the group without complaint, which looks like an answer and is not one. Follow the rule and your queries will work everywhere and mean what they say.
Sorting the groups
ORDER BY comes after GROUP BY. You can sort by the aggregate, usually through its alias.
SELECT city, COUNT(*) AS customers
FROM customers
GROUP BY city
ORDER BY customers DESC, city;Result
| city | customers |
|---|---|
| Delhi | 2 |
| Ludhiana | 2 |
| Mumbai | 2 |
| Amritsar | 1 |
| Chennai | 1 |
| Jaipur | 1 |
| Kochi | 1 |
| Kolkata | 1 |
| Pune | 1 |
9 rows
Add LIMIT for a top list. Which product has been ordered in the largest quantity?
SELECT product_id, SUM(quantity) AS units
FROM order_items
GROUP BY product_id
ORDER BY units DESC
LIMIT 3;Result
| product_id | units |
|---|---|
| 5 | 25 |
| 9 | 12 |
| 6 | 7 |
3 rows
The result shows product numbers, not names, because the names live in another table. Bringing them in is what joins are for.
Filtering before grouping
WHERE runs first. It decides which rows go into the piles at all.
SELECT status, COUNT(*) AS orders
FROM orders
WHERE order_date >= '2026-04-01'
GROUP BY status;Result
| status | orders |
|---|---|
| delivered | 6 |
| pending | 3 |
| shipped | 2 |
3 rows
Only the orders from April onwards are counted. Orders before that never reach a pile.
Grouping by two columns
With two columns in the GROUP BY, there is one pile for each combination that exists.
SELECT state, city, COUNT(*) AS customers
FROM customers
GROUP BY state, city
ORDER BY state, city;Result
| state | city | customers |
|---|---|---|
| Delhi | Delhi | 2 |
| Kerala | Kochi | 1 |
| Maharashtra | Mumbai | 2 |
| Maharashtra | Pune | 1 |
| Punjab | Amritsar | 1 |
| Punjab | Ludhiana | 2 |
| Rajasthan | Jaipur | 1 |
| Tamil Nadu | Chennai | 1 |
| West Bengal | Kolkata | 1 |
9 rows
Grouping by a calculation
You can group by something worked out. This counts orders in each month by cutting the year and month from the date.
SELECT SUBSTR(order_date, 1, 7) AS month, COUNT(*) AS orders
FROM orders
GROUP BY SUBSTR(order_date, 1, 7)
ORDER BY month;Result
| month | orders |
|---|---|
| 2026-01 | 3 |
| 2026-02 | 3 |
| 2026-03 | 3 |
| 2026-04 | 2 |
| 2026-05 | 2 |
| 2026-06 | 2 |
| 2026-07 | 1 |
| 2026-08 | 1 |
| 2026-09 | 2 |
| 2026-10 | 1 |
10 rows
Repeating the calculation in the GROUP BY is the form that works in every database. SQLite, MySQL and PostgreSQL also let you write GROUP BY month, using the alias. SQL Server and older versions of Oracle do not.
Here is another: how many customers joined in each year?
SELECT SUBSTR(joined, 1, 4) AS year, COUNT(*) AS new_customers
FROM customers
GROUP BY SUBSTR(joined, 1, 4);Result
| year | new_customers |
|---|---|
| 2025 | 11 |
| 2026 | 1 |
2 rows
How NULL is grouped
All the rows with NULL in the grouping column form one group of their own.
SELECT manager_id, COUNT(*) AS staff
FROM employees
GROUP BY manager_id;Result
| manager_id | staff |
|---|---|
| NULL | 1 |
| 1 | 3 |
| 2 | 2 |
| 5 | 1 |
| 7 | 1 |
5 rows
The row with no manager number is the person at the top.
Groups with nothing in them do not appear
GROUP BY makes piles only from rows that exist. Three of our customers have never ordered, so they are simply absent here:
SELECT customer_id, COUNT(*) AS orders
FROM orders
GROUP BY customer_id;Result
| customer_id | orders |
|---|---|
| 1 | 4 |
| 2 | 3 |
| 4 | 3 |
| 5 | 2 |
| 7 | 2 |
| 8 | 2 |
| 9 | 2 |
| 11 | 1 |
| 12 | 1 |
9 rows
Nine customers appear, not twelve. A report that should show a zero for the other three needs a LEFT JOIN. Remember this whenever a summary seems to have lost a few rows.
GROUP BY without an aggregate
This works, and returns each value once, exactly like DISTINCT.
SELECT department FROM employees GROUP BY department;Result
| department |
|---|
| Management |
| Sales |
| Support |
| Warehouse |
4 rows
The order in which a query runs
The order in which you write the parts is not the order in which the database does them. Knowing the real order explains most of the rules you have met.
FROM: take the table.WHERE: throw away rows that fail the test.GROUP BY: sort the remaining rows into piles.- Aggregates: work out a value for each pile.
HAVING: throw away piles that fail a test (next lesson).SELECT: choose and name the columns.ORDER BY: sort the result.LIMIT: cut it down.
That is why WHERE cannot use an aggregate or an alias, and ORDER BY can use both.
Try it yourself
Exercise 1. Count the employees in each department. Show the department and the count.
Show one answer
SELECT department, COUNT(*) FROM employees GROUP BY department;| department | COUNT(*) |
|---|---|
| Management | 1 |
| Sales | 3 |
| Support | 2 |
| Warehouse | 2 |
4 rows
Exercise 2. Show the total stock in each product category.
Show one answer
SELECT category, SUM(stock) FROM products GROUP BY category;| category | SUM(stock) |
|---|---|
| Electronics | 253 |
| Grocery | 175 |
| Home | 122 |
| Stationery | 515 |
4 rows
Exercise 3. Count the orders for each status, with the largest count first. Show the status and the count.
Show one answer
SELECT status, COUNT(*) AS orders FROM orders GROUP BY status ORDER BY orders DESC;| status | orders |
|---|---|
| delivered | 13 |
| pending | 3 |
| shipped | 2 |
| cancelled | 2 |
4 rows
Examples on this page were run with SQLite 3.50.4 on 12 November 2026. The results shown are the real results.
This is an independent learning site. It is not connected with the makers of any database product. MySQL, PostgreSQL, SQL Server, Oracle and SQLite are named only so that you know where a piece of SQL works. The shop, its customers and its staff in the practice database are made up.