GROUP BY

GROUP BY splits rows into groups that share a value, then calculates aggregates for each group: revenue per month, employees per department, average price per category. It turns a list of facts into a summary table.

Grouping by one column

SQL
SELECT category, COUNT(*) AS products, round(AVG(price), 2) AS avg_price
FROM products
GROUP BY category;
Result 4 rows
categoryproductsavg_price
Books334.13
Electronics445.99
Home331.83
Toys228.95

Each distinct category becomes one row, and the aggregates are calculated per group.

How it works

  1. FROM and WHERE pick the rows.
  2. GROUP BY sorts them into buckets by the grouping value.
  3. Each bucket becomes one output row; aggregates summarise the bucket.

The rule that follows: every column in the select list must either be in GROUP BY or be inside an aggregate. Otherwise there's no single value to show for the group.

Grouping with WHERE and ORDER BY

SQL
SELECT status, COUNT(*) AS orders
FROM orders
WHERE order_date >= '2026-01-01'
GROUP BY status
ORDER BY orders DESC;
Result 3 rows
statusorders
pending3
shipped2
delivered2

Grouping by several columns

SQL
SELECT country, city, COUNT(*) AS customers
FROM customers
GROUP BY country, city
ORDER BY country, city;
Result 9 rows
countrycitycustomers
CzechiaPrague1
FranceNULL1
FranceLyon1
GermanyBerlin1
PortugalLisbon2
PortugalPorto1
SpainMadrid1
UKLondon1
UKManchester1

A group is formed for each distinct combination. NULLs are grouped together, so Farah gets a France / NULL group.

Grouping by an expression

SQL
SELECT strftime('%Y', hire_date) AS hire_year, COUNT(*) AS hires
FROM employees
GROUP BY hire_year
ORDER BY hire_year;
Result 10 rows
hire_yearhires
20151
20161
20171
20181
20192
20201
20211
20221
20231
20242

Grouping by a calculated value — a year, a price band, the first letter of a name — is common. Standard SQL requires you to repeat the expression in GROUP BY; SQLite, PostgreSQL and MySQL also accept the alias.

SQL
SELECT CASE WHEN price < 20 THEN 'under 20'
            WHEN price < 50 THEN '20–49'
            ELSE '50+' END AS band,
       COUNT(*) AS products
FROM products
GROUP BY band
ORDER BY MIN(price);
Result 3 rows
bandproducts
under 204
20–496
50+2

Revenue per order

SQL
SELECT order_id,
       COUNT(*) AS lines,
       SUM(quantity) AS units,
       round(SUM(quantity * unit_price), 2) AS order_total
FROM order_items
GROUP BY order_id
ORDER BY order_total DESC
LIMIT 5;
Result 5 rows
order_idlinesunitsorder_total
1012119.98
722108.99
42599
151298
121397.5

Monthly report

SQL
SELECT strftime('%Y-%m', o.order_date) AS month,
       COUNT(DISTINCT o.id) AS orders,
       round(SUM(oi.quantity * oi.unit_price), 2) AS revenue
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.id
WHERE o.status <> 'cancelled'
GROUP BY month
ORDER BY month;
Result 7 rows
monthordersrevenue
2025-092109.89
2025-102171.48
2025-11181
2025-122148.89
2026-012203.78
2026-023162
2026-032177.98

This preview uses a join to combine orders with their items — joins are explained from lesson 23. Notice COUNT(DISTINCT o.id): after the join, an order with two items appears twice.

Try it yourself

Show each department's number of employees, total payroll and highest salary.

Try it yourself
Result