Aggregating different subsets of the same data
In the previous articles, we learned two important ideas. GROUP BY lets us summarise multiple rows, and CASE lets us produce different values depending on a condition. Conditional aggregation combines these ideas: it allows us to calculate an aggregate using only the rows that satisfy a particular condition. Consider the same orders table:
Suppose we need a summary showing the total number of orders, the number of paid, pending, and cancelled orders, and the total value of paid orders. One approach would be to write several separate queries, such as SELECT COUNT(*) FROM orders WHERE status = 'paid';, then another for pending, then another for cancelled. This works, but SQL can calculate all of these metrics together. That is where conditional aggregation becomes useful.
Counting rows with SUM(CASE...)
One common conditional aggregation pattern is SUM(CASE WHEN condition THEN 1 ELSE 0 END). Suppose we want to count paid orders:
1 + 0 + 1 + 0 + 1 + 1 = 4 → paid_order_countCASE decides whether each row contributes to the aggregate: 1 for matching rows, 0 for non-matching rows. Then SUM produces the count.Calculating several counts in one query
Once we understand the pattern, we can calculate several metrics from the same rows:
The table is read as one logical input set, while each aggregate applies its own condition. This is one of the main reasons conditional aggregation is useful: we can calculate several related metrics in the same result row.
Conditional SUM: adding values only when a condition matches
Conditional aggregation is not limited to counting rows. Suppose we want the total value of paid orders. The value of each order is quantity * unit_price, and we want that amount to contribute only when status = 'paid':
1500 + 0 + 3600 + 0 + 750 + 3600 = 9450 → paid_order_valueThe pattern is SUM(CASE WHEN condition THEN value ELSE 0 END): for matching rows, contribute the value; for non-matching rows, contribute zero.
Multiple metrics from the same data
Now we can combine several business metrics in one query, such as a dashboard needing total orders, paid, pending, and cancelled counts, total order value, paid order value, and pending order value:
One query now produces several metrics describing the same underlying data.
Conditional aggregation with GROUP BY
Conditional aggregation becomes even more useful when combined with GROUP BY. “For each customer, show total orders, paid orders, pending orders, and total paid order value”:
The result grain is still one row per customer, but each row now contains several measures calculated from different subsets of that customer’s orders. For Ben, order 1002 is pending (3200) and 1006 is paid (3600), so total_orders = 2, paid_orders = 1, pending_orders = 1, paid_order_value = 3600.
Why not put the condition in WHERE?
Consider SELECT customer, COUNT(*) AS paid_orders FROM orders WHERE status = 'paid' GROUP BY customer;. This correctly counts paid orders. But the WHERE clause removes every non-paid order before aggregation begins, so we cannot simultaneously calculate paid, pending, and cancelled counts from the same input rows. For example, WHERE status = 'paid' removes Ben’s pending order before any aggregates are calculated. Conditional aggregation keeps the row available; it simply contributes differently to each metric.
WHERE decides whether a row participates in the query at all. Conditional aggregation decides whether that participating row contributes to a particular metric.COUNT(CASE WHEN...)
There is another common way to perform conditional counting: COUNT(CASE WHEN condition THEN 1 END). Why does this work? Remember that COUNT(expression) counts non-NULL values. The CASE expression produces paid → 1 and not paid → NULL, because there is no ELSE:
COUNT counts the four non-NULL results, so the query returns 4.
The COUNT(CASE...) that counts every row
Suppose someone writes the query below. At first glance, this seems reasonable: for paid orders 1, for everything else 0, so you might expect COUNT to count only the ones. It does not.
The fix
If you want to use COUNT(CASE...), allow non-matching rows to become NULL by dropping the ELSE. You could also explicitly write ELSE NULL, but that is unnecessary because NULL is already the default. Alternatively, use the SUM pattern. Both correctly return 4:
SUM(CASE...)
Match → 1, no match → 0, then add the values.
COUNT(CASE...)
Match → non-NULL, no match → NULL, then count the non-NULL values.
SUM(CASE...) or COUNT(CASE...), and empty sums
For conditional counts, either pattern can be correct. Many teams prefer the SUM form because the 1 and 0 make the counting logic visually explicit. The most important thing is not which style you choose; it is understanding why it works.
SUM without ELSE can return NULL. If we sum CASE WHEN status = 'refunded' THEN quantity * unit_price END and the table contains no refunded orders, the CASE produces NULL for every row, and SUM of all-NULL can be NULL, not 0. If the required metric should be zero when there are no matching rows, provide ELSE 0 so every non-matching row contributes zero.Conditions can use more than one column
The condition inside CASE can be as specific as the business requirement requires. “Total value of paid orders worth at least 3,000”:
The qualifying orders are 1003 → 3600 and 1006 → 3600, so the result is 7200. Conditional aggregation can use the same logical conditions we already learned with WHERE and CASE.
Using FILTER
Some SQL databases support the FILTER clause for aggregates. Instead of a nested CASE, you can write:
Similarly, instead of COUNT(CASE WHEN status = 'paid' THEN 1 END), you can write COUNT(*) FILTER (WHERE status = 'paid'). This makes the intent very clear: calculate this aggregate using only rows that satisfy this condition. Using FILTER, our earlier summary becomes:
FILTER is concise and expressive, but it is not supported uniformly across every SQL database. The CASE approach is more widely portable, so when writing SQL intended to work across several database systems, conditional CASE expressions are often the safer common form. When your database supports FILTER and portability is not a concern, it can make queries with many conditional metrics easier to read. The underlying idea is the same: apply a condition to one aggregate without removing the row from every other aggregate.Conditional aggregation is not the same as GROUP BY
Suppose we want one row per status. GROUP BY status creates several result rows. Conditional aggregation solves a different presentation problem, producing several status-specific metrics as columns in the same result row.
Both queries summarise the same data, but their result shapes are different. Use GROUP BY status when you want one row per status; use conditional aggregation when you want several status-specific metrics as columns in the same result row.
When conditional aggregation is useful
WHERE would remove rows needed by another metric.Look for common mistakes such as:
COUNT(CASE ... ELSE 0 END) unexpectedly counts every row.SUM(CASE...) returns NULL when there are no matching rows.WHERE filter removes rows needed by another conditional metric.COUNT(column) is used when the requirement is actually to count rows.Build a conditional aggregate from four questions
1 · What is the result grain?
One row for the whole dataset (no GROUP BY), or one row per GROUP BY key.
2 · What metric are we calculating?
e.g. paid order count.
3 · Which rows should contribute?
the condition, e.g. status = 'paid'.
4 · What should non-matching rows contribute?
0 for a SUM count; NULL for a COUNT.
One more prediction
Consider these orders. Requirement: return the total number of orders and the number of paid orders in the same result row. Which query is correct?
Lock these in
What is conditional aggregation?
Applying different conditions to different aggregate calculations over the same set of rows.
How do you conditionally count with SUM?
SUM(CASE WHEN condition THEN 1 ELSE 0 END).
How do you conditionally count with COUNT?
COUNT(CASE WHEN condition THEN 1 END).
Why is ELSE 0 wrong inside COUNT(CASE...)?
Because COUNT counts non-NULL values, and zero is still non-NULL.
Why can conditional SUM use ELSE 0?
Because zero means the non-matching row contributes nothing to the sum.
What does FILTER do?
It applies a condition to an individual aggregate where the SQL database supports that syntax.
Why use conditional aggregation instead of WHERE?
Because WHERE removes a row from the entire query, while conditional aggregation can let the same row contribute to some metrics and not others.
Summary
Conditional aggregation combines conditional logic with aggregate functions. For conditional counts, a common pattern is SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END); for conditional sums, SUM(CASE WHEN status = 'paid' THEN quantity * unit_price ELSE 0 END). You can calculate several metrics from the same data, and combine conditional aggregation with GROUP BY. If your database supports FILTER, expressions such as COUNT(*) FILTER (WHERE status = 'paid') can provide a cleaner alternative.
COUNT(CASE...) rule is: COUNT counts non-NULL values, not values equal to 1. So COUNT(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) is wrong for conditional counting, because both 1 and 0 are counted. Use COUNT(CASE WHEN status = 'paid' THEN 1 END) or the SUM form instead.Instead of removing rows globally, decide how each row should contribute to each metric. Next, we will move from summarising one table to combining related data across tables using SQL joins.