Brainlag
  1. Library
  2. Concepts
  3. SQL

Aggregates, GROUP BY and HAVING

COUNT, SUM, AVG, MIN and MAX squash rows into one number. GROUP BY gives one row per group, and HAVING filters the groups after WHERE has filtered the rows.

SQL10 min

Many rows, one number

An aggregate function reads a whole column and gives back one value:

  • COUNT(*) counts rows. COUNT(stock) counts rows where stock is not NULL.
  • SUM(x), AVG(x), MIN(x), MAX(x) do what they say.

SELECT COUNT(*), AVG(price) FROM products; returns one row about the whole shop.

Many rows become one

GROUP BY category sorts the rows into piles, one per category, then runs the aggregates on each pile. You get one row per category.

The rule: every column in SELECT must be in the GROUP BY or inside an aggregate. SELECT category, name, COUNT(*) ... GROUP BY category asks for one name to stand for a whole pile. SQLite quietly picks one, which looks like an answer and is not.

WHERE before, HAVING after

WHERE filters rows, before the piles are made. HAVING filters groups, after the aggregates are worked out, so it can use them:

SQL
SELECT category, COUNT(*) AS products
FROM products
WHERE price < 20
GROUP BY category
HAVING COUNT(*) >= 3;

Read it as: keep the cheap products, pile them by category, keep the piles with at least three.

Run it: the shop by category

Run it, then change one thing and run it again.

Tables customers, products, orders, order_items
customers 8 rows, 3 columns
idnameyear_group
1Amara12
2Ben13
3Chloe12
4Dev12
5Ella13
6Femi13
7Grace10
8Hugo10
products 12 rows, 5 columns
idnamecategorypricestock
1Sketchbook A4art4.530
2Acrylic paintsart8.012
3Brush setart5.2520
4Finelinersart3.7545
5Graph papermaths2.060
6Scientific calculatormaths12.998
7Protractormaths0.9970
8Lab gogglesscience6.515
9Periodic table posterscience3.025
10Hoodiemerch24.010
11Water bottlemerch9.518
12Lanyardmerch2.540
orders 41 rows, 4 columns
idcustomer_idplaced_onstatus
112026-09-16paid
212026-09-03paid
312026-09-21paid
412026-09-03paid
512026-09-20paid
612026-09-27paid
712026-09-16paid
812026-09-09paid
912026-09-08paid
1022026-09-28paid
1122026-09-30refunded
1222026-09-13paid

First 12 of 41 rows. Query the table to see the rest.

order_items 53 rows, 4 columns
idorder_idproduct_idqty
1122
22102
33101
4411
5582
6691
77111
8891
98122
10911
119101
121071

First 12 of 53 rows. Query the table to see the rest.

query.sql to run
SELECT category,
       COUNT(*) AS products,
       ROUND(AVG(price), 2) AS avg_price,
       MAX(price) AS dearest,
       SUM(stock) AS in_stock
FROM products
GROUP BY category
ORDER BY products DESC, category;

Customers who keep coming back

Using orders, return each customer_id with their number of paid orders (status 'paid') in a column named paid_orders. Only keep customers with 6 or more paid orders.

exercise_1.sql to run
SELECT customer_id, COUNT(*) AS paid_orders
FROM orders;

Code editor. Press Control or Command plus Enter to run the code. Tab indents; to move focus out of the editor, press Escape and then Tab.