Beginner: asking questions · Aggregation: GROUP BY and HAVING · topic 19 of 62 · 35 min
Conditional aggregation and string aggregation
Explanation
Why conditional aggregation?
GROUP BY status gives one row per status, going down the page. Managers often want the same numbers across the page instead, as columns: Placed 2, Shipped 1, Delivered 4, Cancelled 1, all in one row. Turning row values into columns like this is called a pivot, and in plain SQL it is done with a CASE inside an aggregate.
SUM(CASE WHEN ...)
CASE WHEN status = 'Delivered' THEN 1 ELSE 0 END gives 1 for delivered rows and 0 for the rest. SUM of those ones and zeros is the number of delivered rows. One such SUM per status makes one column per status.
SELECT SUM(CASE WHEN status = 'Placed' THEN 1 ELSE 0 END) AS placed,
SUM(CASE WHEN status = 'Shipped' THEN 1 ELSE 0 END) AS shipped,
SUM(CASE WHEN status = 'Delivered' THEN 1 ELSE 0 END) AS delivered,
SUM(CASE WHEN status = 'Cancelled' THEN 1 ELSE 0 END) AS cancelled
FROM orders;
-- 2 | 1 | 4 | 1Add GROUP BY and you get a pivot per group. Here, for each category, how many products are in stock and how many are sold out:
SELECT category,
SUM(CASE WHEN stock > 0 THEN 1 ELSE 0 END) AS in_stock,
SUM(CASE WHEN stock = 0 THEN 1 ELSE 0 END) AS out_of_stock
FROM products
GROUP BY category;
-- Electronics | 1 | 1
-- Fashion | 2 | 0
-- Grocery | 2 | 0
-- Kitchen | 2 | 0Two other forms do the same job. COUNT(CASE WHEN cond THEN 1 END) works because CASE gives NULL when nothing matches and COUNT skips NULLs. And SUM(CASE WHEN cond THEN amount ELSE 0 END) adds up an amount for the matching rows only, for example delivered revenue next to total revenue.
FILTER: a cleaner way
SELECT city,
COUNT(*) AS students,
COUNT(*) FILTER (WHERE fees_paid = 1) AS paid
FROM students
GROUP BY city;
-- Chennai | 1 | 1
-- Delhi | 2 | 1
-- Pune | 3 | 2 ... 5 rowsPercentages
A percentage is part × 100.0 / whole, worked out per group. Multiply by 100.0 first so integer division does not cut it to 0 (module 3), then round.
SELECT city, ROUND(100.0 * SUM(fees_paid) / COUNT(*), 1) AS paid_pct
FROM students GROUP BY city;
-- Delhi | 50
-- Pune | 66.7 ... (fees_paid is 1 or 0, so SUM counts the payers)Joining text inside a group
Sometimes you want the members of a group listed in one cell: the products in each category, the team in each department. GROUP_CONCAT(x, separator) in SQLite and MySQL joins the values of a group into one text. The default separator is a comma.
SELECT category, GROUP_CONCAT(name, ', ' ORDER BY name) AS products
FROM products
GROUP BY category;
-- Electronics | Bluetooth Earbuds, Power Bank 10000mAh
-- Kitchen | Pressure Cooker 3L, Steel Tiffin Box ... 4 rows| Database | Comma-separated names, sorted |
|---|---|
| SQLite 3.44+ | GROUP_CONCAT(name, ', ' ORDER BY name) (or string_agg) |
| MySQL | GROUP_CONCAT(name ORDER BY name SEPARATOR ', ') |
| PostgreSQL | STRING_AGG(name, ', ' ORDER BY name) |
| SQL Server 2017+ | STRING_AGG(name, ', ') WITHIN GROUP (ORDER BY name) |
Common mistakes
- Forgetting ELSE 0 in SUM(CASE ...): a group with no matches then shows NULL instead of 0. Use ELSE 0, or COUNT(CASE ... THEN 1 END), which gives 0.
- Writing COUNT(CASE WHEN cond THEN 1 ELSE 0 END): COUNT counts the zeros too, so you get the row count. With COUNT, leave out the ELSE.
- Integer percentages: SUM(x) * 100 / COUNT(*) drops the fraction. Use 100.0.
- Relying on the order inside GROUP_CONCAT without ORDER BY. Add ORDER BY inside the aggregate.
- Hard-coding a pivot column for each status and forgetting a new status added later. Check with a plain GROUP BY status that the columns add up to the total.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Conditional aggregation and string aggregation · 1/7
GROUP BY gives numbers going down the page. Managers often want them across the page, as columns. That is a pivot.
Prefer a video by a teacher? Find free videos on “sql conditional aggregation sum case when pivot group_concat string_agg” on YouTube ↗
In real life: An order status board
Operations teams at delivery and e-commerce companies watch a status board: per city or warehouse, how many orders are placed, shipped, delivered and cancelled. Each column is a conditional count, and a total column next to them lets anyone check that the parts add up.
SELECT customer_id,
COUNT(*) AS total,
SUM(CASE WHEN status = 'Delivered' THEN 1 ELSE 0 END) AS delivered,
SUM(CASE WHEN status = 'Cancelled' THEN 1 ELSE 0 END) AS cancelled
FROM orders
GROUP BY customer_id;Practice (0/4 done)
For each category, return category, the number of products with stock above 0 named in_stock, and the number with stock 0 named out_of_stock.
Make a one-row status board from orders with four columns in this order: placed, shipped, delivered, cancelled (the number of orders with each status).
For each category, return category and the names of its products joined with ', ' in alphabetical order, named products. Use GROUP_CONCAT with ORDER BY inside it.
For each city, return city, the number of students named students, the number who paid fees (fees_paid = 1) named paid, and the percentage who paid rounded to 1 decimal place named paid_pct.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Writing reports with aggregationAggregation: GROUP BY and HAVING · same module
- GROUP BY and HAVINGBackend Development · SQL in depth
- Counting and totals: aggregate functionsBackend Development · Databases and SQL basics
- Joining three or more tablesJOINs
- CASE expressionsFunctions, CASE and NULL
- Join pitfalls and how to avoid themJOINs
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What does SUM(CASE WHEN status = 'Placed' THEN 1 ELSE 0 END) give?
Answer: The number of rows with status Placed
Matching rows add 1, the others add 0.
Why is COUNT(CASE WHEN stock = 0 THEN 1 ELSE 0 END) wrong for counting sold-out products?
Answer: COUNT counts the 0s too, because 0 is not NULL, so it returns the number of all rows
With COUNT, leave out ELSE so non-matches become NULL and are skipped.
Which database does NOT support the FILTER (WHERE ...) clause on aggregates?
Answer: MySQL
MySQL and SQL Server need SUM(CASE ...) instead.
What is the PostgreSQL way to list names per group, separated by commas, in order?
Answer: STRING_AGG(name, ', ' ORDER BY name)
PostgreSQL uses STRING_AGG with ORDER BY inside the brackets.
GROUP_CONCAT(name) without ORDER BY: in what order are the names?
Answer: Not guaranteed; add ORDER BY inside the aggregate
Only ORDER BY inside the aggregate fixes the order.
You write SELECT 100.0 * COUNT(*) / COUNT(*) FROM orders WHERE status = 'Delivered' to get the delivered share. What is wrong?Related
Answer: WHERE leaves only delivered rows, so the answer is always 100
Count the part with CASE and the whole with COUNT(*), without filtering the whole away.
Why does PostgreSQL reject SELECT city, name, SUM(amount) FROM t GROUP BY city?Related
Answer: name is neither grouped nor aggregated, so one city has many possible names
Each output row is one group. A column must be the same for the whole group (grouped) or combined from it (aggregated).
From GROUP BY and HAVING
A table has 10 rows; the email column is NULL in 3 of them. What is COUNT(email)?Related
Answer: 7
COUNT(column) counts only rows where the column has a value: 10 - 3 = 7.
orders JOIN order_items: what does one row of the result stand for?Related
Answer: One order line
The grain becomes the more detailed table, order_items.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 1 more topic in “Aggregation: GROUP BY and HAVING” before its exam: Writing reports with aggregation.
- Module 4 exam: Aggregation, GROUP BY and HAVING · 15 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, JOINs. 62 topics left in the course before the final exam.