Filter after aggregation with HAVING and pivot your data with CASE WHEN inside aggregate functions.
Open this lesson in KodokonYou already know GROUP BY. This module tackles what separates a correct query from a professional one: post-aggregation filtering, conditional aggregates, and a solid grasp of evaluation order. To practice, open sqliteonline.com (SQLite engine) or launch sqlite3 in a terminal: each lesson provides the CREATE TABLE and INSERT statements you need. Copy this dataset before running the examples.
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer TEXT NOT NULL,
category TEXT NOT NULL,
amount REAL NOT NULL,
status TEXT NOT NULL
);
INSERT INTO orders (customer, category, amount, status)
VALUES
('alice', 'books', 42.0, 'paid'),
('alice', 'games', 55.0, 'paid'),
('alice', 'books', 18.5, 'refunded'),
('bruno', 'games', 70.0, 'paid'),
('bruno', 'books', 12.0, 'paid'),
('chloe', 'games', 95.0, 'paid'),
('chloe', 'books', 30.0, 'paid'),
('david', 'books', 8.0, 'refunded');The logical order of evaluation is the key: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY. WHERE filters rows before grouping; HAVING filters groups after the aggregates are computed. That is why WHERE SUM(amount) > 90 is illegal: at the WHERE stage, no sum exists yet. Here, we keep only customers whose paid total exceeds 90 (expected: alice and chloe).
SELECT customer,
COUNT(*) AS nb_orders,
SUM(amount) AS total
FROM orders
WHERE status = 'paid'
GROUP BY customer
HAVING SUM(amount) > 90;The second must-know pattern is the conditional aggregate. By slipping a CASE WHEN inside SUM or COUNT, you compute several metrics in a single pass over the table, where a beginner would run three separate queries. This is the standard technique for pivoting rows into columns.
SELECT customer,
SUM(CASE WHEN category = 'books'
THEN amount ELSE 0 END) AS books_total,
SUM(CASE WHEN category = 'games'
THEN amount ELSE 0 END) AS games_total
FROM orders
WHERE status = 'paid'
GROUP BY customer;