Step 1 Understand it
Why you need it. "For each customer, how many orders were delivered and how many weren't?" Running two and gluing them together is slow and messy. Interviewers love this pattern because it shows you understand that work on whatever values you give them.
Watch it happen
Read the animation as text
- One pass over the orders, several different counts per customer. The trick: hand SUM a CASE.
- For each row, the CASE gives 1 if the order is delivered and 0 if not. (Shown here as is_deliv.)
- SUM adds up the 1s and 0s in each group — which is exactly the number of delivered orders. A second SUM with the opposite condition counts the rest; COUNT(*) counts them all.
- Swap the conditions for months and the rows become columns — that is a pivot. For a percentage, divide a conditional count by COUNT(*), using 100.0 to avoid integer division.
Picture it
flowchart LR R["Priya's orders<br/>delivered, delivered,<br/>delivered, shipped, delivered"] --> C["CASE gives<br/>1, 1, 1, 0, 1"] C --> S["SUM = 4 delivered"] R --> T["COUNT(*) = 5 orders"]
The idea
An aggregate adds up whatever you hand it. Hand SUM a CASE that gives 1 for the rows you care about and 0 for the rest, and it counts just those rows. Hand it CASE WHEN … THEN amount END and it sums just those amounts. Do this several times in one , with different conditions, and you get several from one pass over the data. Use it with months as the conditions and the rows become columns — that is a . For a percentage, divide one conditional count by COUNT(*), using 100.0 to avoid .
The shape of it
SELECT group_column,
SUM(CASE WHEN condition THEN 1 ELSE 0 END) AS how_many,
SUM(CASE WHEN condition THEN value END) AS how_much
FROM table_name
GROUP BY group_column;Words in lower case are the parts you fill in.
How it works, step by step
- customer_id makes one group per customer.
- For each row, the CASE gives 1 if the order is delivered and 0 if not.
- SUM adds those 1s and 0s, which is the number of delivered orders in the group.
- A second SUM with the opposite condition counts the rest.
COUNT(*)gives the total. - For a pivot, each column's CASE picks one month:
CASE WHEN order_date LIKE '2024-01%' THEN amount ELSE 0 END.
Analogy
Counting a class with tally marks on a sheet with several columns: as each pupil walks past, you add a mark under "walked", "cycled" or "came by bus". One walk-past fills in every column at once.
Where the analogy breaks
With tally marks a pupil can only go in one column. has no such rule: one row can count towards several columns if several conditions are true, so make your conditions not overlap when they should add up to the total.
Worked examples
Several counts in one query
SELECT customer_id,
COUNT(*) AS orders,
SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) AS delivered,
SUM(CASE WHEN status <> 'delivered' THEN 1 ELSE 0 END) AS not_delivered
FROM orders
GROUP BY customer_id
ORDER BY customer_id;4 rows
| customer_id | orders | delivered | not_delivered |
|---|---|---|---|
| 1 | 5 | 4 | 1 |
| 2 | 2 | 2 | 0 |
| 3 | 2 | 1 | 1 |
| 5 | 1 | 0 | 1 |
A pivot: one column per month
SELECT customer_id,
SUM(CASE WHEN order_date LIKE '2024-01%' THEN amount ELSE 0 END) AS jan,
SUM(CASE WHEN order_date LIKE '2024-02%' THEN amount ELSE 0 END) AS feb,
SUM(CASE WHEN order_date LIKE '2024-03%' THEN amount ELSE 0 END) AS mar
FROM orders
GROUP BY customer_id
ORDER BY customer_id;4 rows
| customer_id | jan | feb | mar |
|---|---|---|---|
| 1 | 245 | 150 | 30 |
| 2 | 200 | 0 | 75 |
| 3 | 0 | 60 | 0 |
| 5 | 0 | 0 | 33 |
Rows became columns. Lena's February is 60: order 106's amount is skipped by SUM.
A percentage
SELECT ROUND(100.0 * SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) / COUNT(*), 1) AS pct_delivered,
AVG(CASE WHEN status = 'delivered' THEN 1.0 ELSE 0 END) AS share_delivered
FROM orders;1 row
| pct_delivered | share_delivered |
|---|---|
| 70 | 0.7 |
7 of 10 orders were delivered. 100.0 (not 100) stops integer division. The AVG of 1s and 0s is a neat shortcut for a share.
Must-know points
SUM(CASE WHEN cond THEN 1 ELSE 0 END)counts matching rows;COUNT(CASE WHEN cond THEN 1 END)does the same, because skips the NULLs.SUM(CASE WHEN cond THEN amount END)sums only matching amounts.- One pass over the data gives many totals — faster and simpler than several queries.
- A pivot is conditional aggregation with one column per value.
- For percentages, multiply by 100.0 before dividing.
Common mistakes
- Writing
COUNT(CASE WHEN cond THEN 1 ELSE 0 END). COUNT counts the 0s too, because 0 is not NULL. Use SUM, or drop the ELSE. - Computing a percentage with whole numbers:
100 * 7 / 10is fine but7 / 10 * 100is 0. - Conditions that overlap when the columns should add up to the total.
In other databases
- and support a clause:
COUNT(*) FILTER (WHERE status = 'delivered'). It is the same idea with neater syntax. - and Oracle have a PIVOT operator, but CASE works everywhere and interviewers accept it.
Step 2 Go further (free, optional)
Optional. The notes above are written to be enough on their own. These free resources give a second explanation and more practice.
- Read
-
Read
PostgreSQL docs — Aggregate Expressions (FILTER)
official documentation, free
-
Practice
DataLemur — conditional aggregation questions
real interview questions, many free
Step 3 Practise
Try each one yourself first: out loud, or written and run in the practice box below, which saves every query you run. Then open the answer and compare.
-
ExplainSay what it does and when to use it, in plain words, with no notes.
Explain how
SUM(CASE WHEN … THEN 1 ELSE 0 END)counts rows.Show answer
The CASE runs once for every row and turns it into a number: 1 if the row matches, 0 if it doesn't. SUM then adds up those 1s and 0s. Every match adds 1 and every other row adds nothing, so the total is the number of matching rows.
Because each CASE is its own column, one query can count several different things at once, each with its own condition.
-
Writefrom a blank page, then run it
For each department, show the number of employees earning 60000 or more and the number earning less, in one query.
Show answer
Two SUM(CASE …) columns, one per condition, grouped by department. "60000 or more" is
>= 60000, so Dara (exactly 60,000) counts in the first column.sqlSELECT dept_id, SUM(CASE WHEN salary >= 60000 THEN 1 ELSE 0 END) AS earn_60k_or_more, SUM(CASE WHEN salary < 60000 THEN 1 ELSE 0 END) AS earn_less FROM employees GROUP BY dept_id ORDER BY dept_id;4 rows
dept_id earn_60k_or_more earn_less NULL 0 1 1 4 0 2 1 1 3 0 1 -
Predictsay it before you run it
What does
SELECT COUNT(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) FROM orders;return, and why is it not 7?Show answer
10, not 7.
COUNT counts every value that is not NULL. The CASE gives 1 or 0, and 0 is a real value, not NULL, so COUNT counts all 10 rows.
Two fixes: use SUM, which adds the 1s and 0s; or keep COUNT but drop the ELSE, so non-matching rows become NULL and are skipped. Both give 7.
sqlSELECT COUNT(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) AS counted, COUNT(CASE WHEN status = 'delivered' THEN 1 END) AS fixed_count, SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) AS fixed_sum FROM orders;1 row
counted fixed_count fixed_sum 10 7 7 -
Edge casesSay what happens with NULLs, ties, duplicates or no rows at all, and fix it.
Write the share of orders that are cancelled, as a percentage with one decimal place, and check it is 10.0.
Show answer
1 cancelled order out of 10 is 10.0%. Start with 100.0, not 100, so the division is exact. With whole numbers the fraction is cut off: 1 out of 3 would show 33, not 33.3. (SQLite shows 10.0 as 10.)
sqlSELECT ROUND(100.0 * SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) / COUNT(*), 1) AS pct_cancelled FROM orders;1 row
pct_cancelled 10
Practice box: write and run your answers here
Your queries in this box
Step 4 Interview-ready, then prove it
Your interview answer
About 20 to 40 seconds, said out loud.
Conditional aggregation puts a CASE expression inside an aggregate, like SUM of CASE WHEN status is delivered THEN 1 ELSE 0, so I can compute several metrics per group in a single pass — counts by status, revenue by month as columns, or a ratio. For ratios I multiply by 100.0 first to avoid integer division, and I make sure SUM rather than COUNT wraps a CASE that returns zero.
Follow-up questions
Why is COUNT(CASE WHEN x THEN 1 ELSE 0 END) wrong?
COUNT counts every non-NULL value, and 0 is not NULL, so it counts all rows. Use SUM, or remove the ELSE so non-matches become NULL.
How do you pivot rows into columns in ?
GROUP BY the row label and write one SUM(CASE WHEN column = value THEN amount END) per output column.
Tick each check when you can do it without notes
A topic is finished when you pass all four checks, not when you have read it. Be strict: if you had to look at the notes, it is not ticked yet.
- Explain Say what it does and when to use it, in plain words, with no notes.
- Write Write the query from a blank page, then run it to check.
- Predict Read a query and say which rows it returns before you run it.
- Edge cases Say what happens with NULLs, ties, duplicates or no rows at all, and fix it.
0 of 4 checks ticked.
Saved in this browser. Back it up or move it to another device.