SQL Foundations · 12 min · 150 XP

GROUP BY and HAVING

Count, sum and average per group, and filter before or after grouping.

Aggregate functions (COUNT, SUM, AVG, MIN, MAX) collapse many rows into one number. GROUP BY says what the number is per: per status, per customer, per month.

sql
SELECT status, COUNT(*) AS orders
FROM orders
GROUP BY status
ORDER BY orders DESC, status;

-- status    orders
-- paid      11
-- pending   2
-- refunded  2

Every column in SELECT must either appear in GROUP BY or be inside an aggregate. SQLite lets you break this rule and quietly takes a value from some row in the group. Most databases refuse the query, and you shouldn't rely on either behaviour.

Three different counts. COUNT(*) counts rows. COUNT(city) counts rows where city isn't NULL. COUNT(DISTINCT customer_id) counts different values. The shop has 11 paid orders, but only 7 different customers placed them.

WHERE filters rows before grouping; HAVING filters groups after. Paid orders only is a condition on rows, so it goes in WHERE. Customers with at least two paid orders is a condition on a group's count, so it goes in HAVING.

sql
SELECT customer_id, COUNT(*) AS paid_orders
FROM orders
WHERE status = 'paid'
GROUP BY customer_id
HAVING COUNT(*) >= 2;

-- customer_id  paid_orders
-- 1            3
-- 3            2
-- 4            2

Revenue is the money from items on paid orders: SUM(quantity * unit_price) over order_items, joined to orders so refunded and pending ones can be dropped. Use unit_price, the price actually paid, not the product's list price. Order 109 sold shoes at a discount.

Loading your workspace…