Hook: Conditional aggregation is the SQL version of sorting mail into labeled bins before you count it — one scan, many answers.
Question: What is conditional aggregation in SQL?
Answer: Conditional aggregation means using a condition inside an aggregate function so you can compute different totals from the same grouped rows. The usual pattern is CASE WHEN ... THEN ... END inside SUM, COUNT, or AVG. It is useful because you can produce several metrics in one query without running separate queries for each metric.
Interview-Ready Answer: I use conditional aggregation when I want multiple metrics from the same grouped data, like paid orders, refunded orders, and revenue in one pass. The core idea is to put a CASE expression inside an aggregate: for example, SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) or COUNT(CASE WHEN status = 'paid' THEN 1 END). My main detail to mention is that COUNT ignores NULL, so the ELSE branch matters a lot — a wrong ELSE 0 can make the count incorrect.
Conditional aggregation is a way to make one grouped query answer several business questions at once. The CASE expression acts like a tiny per-row rule: if the row matches the condition, it contributes a value; otherwise it contributes NULL or 0, depending on the metric you want.
WHERE. Any row removed here never reaches the aggregate, so this step changes both totals and denominators.GROUP BY. Think of this as putting rows into buckets such as one bucket per customer, country, or day.CASE is evaluated for each row in each bucket. A CASE expression is just a rule engine that returns a value row by row.SUM adds numbers, COUNT(expr) counts non-NULL values, and AVG averages only the non-NULL values.ELSE branch mattersFor COUNT(CASE WHEN condition THEN 1 END), non-matching rows become NULL and are ignored, so the count is correct. For SUM(CASE WHEN condition THEN amount ELSE 0 END), non-matching rows contribute zero, so the total stays numeric even if nothing matches. But SUM(CASE WHEN condition THEN amount END) can return NULL when no row matches, because the aggregate only sees NULL values.
| Pattern | Best for | Trade-off |
|---|---|---|
| CASE inside aggregate | Portable SQL | Most common; a bit verbose |
| FILTER (WHERE ...) | Modern DBs | Cleaner, less portable |
| Separate subqueries | Very different logic | More scans, harder to maintain |
FILTER is part of the SQL standard and reads nicely, for example SUM(amount) FILTER (WHERE status = 'paid'). But CASE works in more databases, so interviewers often expect that form first.
In practice, conditional aggregation is usually a single pass over the input rows, so the scan cost is roughly O(n) for n rows, plus grouping overhead. If the database uses a hash aggregate, memory is roughly O(g) for g groups; if it uses a sort aggregate, you may also pay for sorting. A very practical win is that one query with three conditional metrics can avoid three separate table scans, which matters a lot on tables with millions of rows and noticeable disk I/O.
COUNT gives 0, but SUM may give NULL unless you use ELSE 0 or COALESCE.WHERE, you may accidentally shrink the total you wanted to compare against.COUNT trap: COUNT ignores only NULL, so returning 0 in the false branch makes the row count, which is usually wrong.Memory hook: Label first, tally second. CASE labels each row; the aggregate only counts the labeled result.
Imagine a checkout service for an e-commerce platform. Every minute, the analytics job needs to report paid orders, refunded orders, pending orders, and paid revenue per merchant. Conditional aggregation lets the team compute all of that from the same orders table in one grouped query, which keeps the dashboard fast and consistent.
What goes wrong when someone misunderstands it? A developer writes COUNT(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) thinking the ELSE 0 means false, but COUNT counts every non-NULL value, so every row gets counted as paid. The symptom is a dashboard where paid orders suddenly equal total orders for every merchant. There is no SQL error, just suspiciously perfect metrics, and the business may make bad decisions because the conversion rate looks artificially high.
How it shows up: the logs look normal, the query finishes fast, but the numbers do not match the raw order totals. SREs and analysts usually spot it when the paid count becomes identical to the total count or when refunds appear to exceed the number of successful orders.
WITH orders AS (
SELECT 1 AS order_id, 101 AS customer_id, 'paid' AS status, 50 AS amount
UNION ALL SELECT 2, 101, 'refunded', 50
UNION ALL SELECT 3, 101, 'paid', 30
UNION ALL SELECT 4, 102, 'pending', 20
UNION ALL SELECT 5, 102, 'pending', 15
UNION ALL SELECT 6, 103, 'paid', 99
UNION ALL SELECT 7, 104, NULL, 40
UNION ALL SELECT 8, 105, 'cancelled', NULL
)
SELECT
customer_id,
COUNT(*) AS total_orders,
COUNT(CASE WHEN status = 'paid' THEN 1 END) AS paid_orders,
SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS paid_revenue_safe,
SUM(CASE WHEN status = 'paid' THEN amount END) AS paid_revenue_nullable,
COUNT(CASE WHEN status = 'refunded' THEN 1 END) AS refunded_orders,
COUNT(CASE WHEN status IS NULL THEN 1 END) AS unknown_status_orders
FROM orders
GROUP BY customer_id
ORDER BY customer_id;
-- Edge case notes:
-- 1) customer_id = 102 has no paid rows, so paid_revenue_nullable returns NULL.
-- If your report needs zero instead of NULL, use ELSE 0 or COALESCE(..., 0).
-- 2) The common bug below is WRONG because COUNT only ignores NULL.
-- Returning 0 in the false branch makes every row count.
--
-- SELECT
-- customer_id,
-- COUNT(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) AS wrong_paid_orders
-- FROM orders
-- GROUP BY customer_id;Follow-up & Tricky Questions:
WHERE? WHERE removes rows before grouping, so it changes the population you are measuring. Conditional aggregation keeps the row in the group and only changes how that row contributes to one metric.FILTER (WHERE ...) instead of CASE? Use FILTER when your database supports it and you want cleaner syntax. Use CASE when you want maximum portability across SQL engines.COUNT(CASE WHEN condition THEN 1 END) or SUM(CASE WHEN condition THEN 1 ELSE 0 END). Both work, but the COUNT version is often easier to read for pure counts.NULLIF(total, 0) so you do not divide by zero.CASE expression per metric, such as paid, refunded, pending, and cancelled.COUNT(CASE WHEN ... THEN 1 ELSE 0 END) a trap? Because COUNT counts any non-NULL value, and 0 is still a value. That means every row is counted, even the ones that fail the condition.SUM(CASE WHEN ... THEN amount END) return NULL? If nothing matches, the CASE returns only NULL values, and SUM over all-NULL input returns NULL. Use ELSE 0 or wrap the result in COALESCE.CASE clause make the database read fewer rows? No. It changes values after rows are read. If you want fewer rows scanned, use a real filter in WHERE or pre-filter in a subquery.Common Mistakes:
COUNT(... ELSE 0): correction — return NULL in the false branch, or omit ELSE entirely, so only matching rows are counted.ELSE 0 for sums: correction — use ELSE 0 or COALESCE when you need a zero instead of NULL.Memory Hook: Label first, tally second. The CASE expression puts each row into a bucket; the aggregate counts the bucket contents.
Cheat Sheet:
SUM(CASE WHEN condition THEN value ELSE 0 END) for safe conditional totals.COUNT(CASE WHEN condition THEN 1 END) for conditional row counts.AVG(CASE WHEN condition THEN value END) averages only matching rows.COUNT ignores NULL; SUM ignores NULL but may return NULL if nothing matches.FILTER (WHERE ...) is cleaner when your DB supports it, but CASE is more portable.Practice Tasks:
NULLIF.FILTER (WHERE ...) if your database supports it.