Practice10problems on this pattern, easy to hardStart practising →
Pattern 6SQL course / Summarising Data

SQL conditional aggregation: calculating multiple metrics from the same data

Learn how to combine CASE with SUM and COUNT, calculate several business metrics in one query, use FILTER where supported, and avoid counting rows that do not actually match your condition.

8 min read12 sections2 predictions10 practice problems
01

Why Conditional Aggregation?

In P04 and P05, 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:

Orders6 rows
order_idcustomerproductquantityunit_pricediscount_amountstatus
1001AshaWireless Mouse2750150paid
1002BenMechanical Keyboard13200NULLpending
1003CarlaLaptop Sleeve31200300paid
1004DevOffice Chair18500NULLcancelled
1005AshaWireless Mouse1750NULLpaid
1006BenMonitor Stand21800200paid

Suppose the requirement is:

RequirementReturn 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 this one, then another for pending, then another for cancelled:

Query
SELECT COUNT(*)
FROM orders
WHERE status = 'paid';

This works, but SQL can calculate all of these metrics together. That is where conditional aggregation becomes useful.

02

Counts and Totals with SUM(CASE)

One common conditional aggregation pattern is SUM(CASE WHEN condition THEN 1 ELSE 0 END). Suppose the requirement is:

RequirementCount the paid orders.
Query
SELECT
SUM(
CASE
WHEN status = 'paid' THEN 1
ELSE 0
END
) AS paid_order_count
FROM orders;
how it works: CASE turns each row into 1 or 06 rows
order_idstatusCASE result
1001paid1
1002pending0
1003paid1
1004cancelled0
1005paid1
1006paid1
1 + 0 + 1 + 0 + 1 + 1 = 4  →  paid_order_count
Tip
CASE 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:

Query
SELECT
COUNT(*) AS total_orders,
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) AS paid_orders,
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending_orders,
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) AS cancelled_orders
FROM orders;
Result1 row
total_orderspaid_orderspending_orderscancelled_orders
6411

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. Each order’s value is quantity * unit_price, and we want the total only for paid orders:

Query
SELECT
SUM(
CASE
WHEN status = 'paid'
THEN quantity * unit_price
ELSE 0
END
) AS paid_order_value
FROM orders;
value contributed per row6 rows
order_idstatusorder_amountcontributed
1001paid15001500
1002pending32000
1003paid36003600
1004cancelled85000
1005paid750750
1006paid36003600
1500 + 0 + 3600 + 0 + 750 + 3600 = 9450  →  paid_order_value

The pattern is SUM(CASE WHEN condition THEN value ELSE 0 END).

Multiple metrics from the same data

Now we can combine several business metrics in one query. Suppose a dashboard needs:

RequirementReturn the total number of orders, the paid, pending, and cancelled counts, the total order value, the paid order value, and the pending order value.
Query
SELECT
COUNT(*) AS total_orders,
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) AS paid_orders,
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending_orders,
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) AS cancelled_orders,
SUM(quantity * unit_price) AS total_order_value,
SUM(CASE WHEN status = 'paid'
THEN quantity * unit_price ELSE 0 END) AS paid_order_value,
SUM(CASE WHEN status = 'pending'
THEN quantity * unit_price ELSE 0 END) AS pending_order_value
FROM orders;
Result1 row
total_orderspaid_orderspending_orderscancelled_orderstotal_order_valuepaid_order_valuepending_order_value
64112115094503200

One query now produces several metrics describing the same underlying data.

03

Conditional Metrics with GROUP BY

Conditional aggregation becomes even more useful when combined with GROUP BY. Suppose the requirement is:

RequirementFor each customer, return the total orders, paid orders, pending orders, and total paid order value.
Query
SELECT
customer,
COUNT(*) AS total_orders,
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) AS paid_orders,
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending_orders,
SUM(CASE WHEN status = 'paid'
THEN quantity * unit_price ELSE 0 END) AS paid_order_value
FROM orders
GROUP BY customer
ORDER BY customer;
Result4 rows
customertotal_orderspaid_orderspending_orderspaid_order_value
Asha2202250
Ben2113600
Carla1103600
Dev1000

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 this query:

Query
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.

Tip
WHERE decides whether a row participates in the query at all. Conditional aggregation decides whether that participating row contributes to a particular metric.
04

Counting with COUNT(CASE)

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:

Query
SELECT
COUNT(
CASE
WHEN status = 'paid' THEN 1
END
) AS paid_orders
FROM orders;
CASE result6 rows
order_idstatusCASE result
1001paid1
1002pendingNULL
1003paid1
1004cancelledNULL
1005paid1
1006paid1

COUNT counts the four non-NULL results, so the query returns 4.

05

The COUNT(CASE) Trap

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.

Pause and predictPredict before you continue

What does the query return?

The query
SELECT
COUNT(
CASE
WHEN status = 'paid' THEN 1
ELSE 0
END
) AS paid_orders
FROM orders;

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.

Query
COUNT(CASE WHEN status = 'paid' THEN 1 END)
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END)
06

SUM, COUNT, and NULL

For conditional counts, either pattern can be correct. Many teams prefer the SUM form because the 1 and 0 make the counting logic easy to understand. The most important thing is not which style you choose; it is understanding why it works.

Tip
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. Suppose the requirement is:

RequirementReturn the total value of paid orders worth at least 3,000.
Query
SELECT
SUM(
CASE
WHEN status = 'paid'
AND quantity * unit_price >= 3000
THEN quantity * unit_price
ELSE 0
END
) AS high_value_paid_order_value
FROM orders;

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.

07

Using FILTER with Aggregates

Some databases support FILTER, a shorter way to calculate aggregates for rows that meet a specific condition.

For example, both expressions count paid orders:

Using CASE
COUNT(CASE WHEN status = 'paid' THEN 1 END)
Using FILTER
COUNT(*) FILTER (WHERE status = 'paid')

The FILTER condition applies only to that aggregate, leaving other calculations in the query unaffected.

You can also use it with SUM:

Query
SUM(quantity * unit_price)
FILTER (WHERE status = 'paid')
Tip
Database Compatibility
FILTER is supported by PostgreSQL, DuckDB, SQLite, Spark SQL, and Trino, among others. MySQL, SQL Server, and Snowflake do not support this aggregate syntax.

For SQL that needs to work across different databases, conditional aggregation using CASE is generally the more portable approach.
08

Rows vs. Columns in Results

GROUP BY and conditional aggregation can summarise the same data in different ways. The difference is how the results are organised.

Using GROUP BY

Suppose we want to count orders by their status.

Grouping by status returns one row for each status.

statusorder_count
cancelled1
paid4
pending1

We get three rows because there are three different statuses.

Using Conditional Aggregation

Now suppose we want the number of paid, pending, and cancelled orders in a single row. Conditional aggregation lets us calculate each count separately and return them as columns.

paid_orderspending_orderscancelled_orders
411

The counts are the same, but the result contains one row with three columns.

Can we use both together?

Yes. We can combine GROUP BY with conditional aggregation.

For example, GROUP BY customer gives us one row per customer. Within each group, conditional aggregation calculates separate counts for paid and pending orders. This is what we did in Section 03.

Remember: GROUP BY determines which rows belong together. Conditional aggregation determines which rows contribute to each metric. We can use both when we need several metrics for each group.

09

Four Questions Before Writing SQL

Before writing a conditional aggregate, answer these four questions.

1. What should each result row represent?
Do we need one summary row for all orders or one row per customer? Use GROUP BY when the result needs separate rows for each group.

2. What do we want to calculate?
For example, we want to count the number of paid orders.

3. Which rows should contribute?
Only orders where status = 'paid' should contribute to the paid order count.

4. What should other rows contribute?
This depends on the aggregate:

  • With SUM(CASE), matching rows contribute 1, and other rows contribute 0.
  • With COUNT(CASE), matching rows produce 1, and other rows produce NULL, which COUNT ignores.

Remember: First decide what each result row represents. Then choose the metric, its condition, and what non-matching rows should contribute.

10

Predict the Correct Query

Consider these orders, with each order’s amount already calculated:

orders6 rows
order_idcustomerstatusorder_amount
1001Ashapaid1500
1002Benpending3200
1003Carlapaid3600
1004Devcancelled8500
1005Ashapaid750
1006Benpaid3600
RequirementFor each customer, return the number of paid orders and the total value of pending orders. Every customer must appear, with 0 where nothing matches.
Pause and predictPredict before you continue

Which query is correct?

The query
-- Option A
SELECT customer,
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) AS paid_orders,
SUM(CASE WHEN status = 'pending' THEN order_amount END) AS pending_value
FROM orders
GROUP BY customer;
-- Option B
SELECT customer,
COUNT(*) AS paid_orders,
SUM(CASE WHEN status = 'pending' THEN order_amount ELSE 0 END) AS pending_value
FROM orders
WHERE status = 'paid'
GROUP BY customer;
-- Option C
SELECT customer,
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) AS paid_orders,
SUM(CASE WHEN status = 'pending' THEN order_amount ELSE 0 END) AS pending_value
FROM orders
GROUP BY customer;
-- Option D
SELECT customer, status,
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) AS paid_orders,
SUM(CASE WHEN status = 'pending' THEN order_amount ELSE 0 END) AS pending_value
FROM orders
GROUP BY customer, status;
11

Using Conditional Aggregation Effectively

Conditional aggregation is useful when a report needs multiple metrics from the same data. For example, you might need to:

  • Count paid, pending, and cancelled orders in one report.
  • Calculate revenue separately for different order statuses.
  • Compare these metrics across customers, products, or stores.

Instead of writing separate queries for each condition, conditional aggregation lets you calculate these metrics together.

Common Mistakes

A query can run successfully and still return incorrect totals. Watch for these mistakes:

1. Using ELSE 0 inside COUNT(CASE)
COUNT includes every non-NULL value, including 0. Adding ELSE 0 makes non-matching rows count too.

2. Missing ELSE 0 in SUM(CASE)
Without ELSE, non-matching rows contribute NULL. If a group has rows but none match the condition, the sum returns NULL instead of 0. Use ELSE 0 when zero is the intended result.

3. Filtering rows too early
A WHERE condition removes rows before aggregation. If a report needs metrics for multiple statuses, filtering to one status can exclude rows needed by other calculations.

4. Using COUNT(column) to count all rows
COUNT(column) ignores rows where that column is NULL. Use COUNT(*) when every row should count.

Key Takeaways

  • Count matching rows: Use SUM(CASE WHEN ... THEN 1 ELSE 0 END) or COUNT(CASE WHEN ... THEN 1 END).
  • Calculate conditional totals: Use SUM(CASE WHEN ... THEN amount ELSE 0 END) to sum values for matching rows.
  • Understand GROUP BY: It determines what each result row represents, while conditional aggregation calculates metrics within that group.
  • Know CASE vs. WHERE: CASE can control which values contribute to an aggregate. WHERE controls which rows are available to the entire query.
  • Know your SQL dialect: FILTER offers a shorter alternative in supported databases, while CASE works across more SQL systems.
12

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.

Instead of removing rows globally, decide how each row should contribute to each metric. Next, in P07 we will move from summarising one table to combining related data across tables using SQL joins.

Practise this pattern

Now write it yourselfAll 10 Conditional Aggregation problems →

Graded problems on this pattern, easy to hard. They run in your browser against real tables – no sign-up needed.

Up next · Pattern 7SQL joins

Next, we move from summarising one table to combining related data across tables with SQL joins.

Go deeper

Ask Colearn about this pattern

Not sure why a conditional count is off, or whether to use CASE or FILTER? Ask about the concept, or paste a simplified version of your query.

Instant answers, grounded in the same verified material the diagnostic grades against.

3 of 3 free questions left
Why does my COUNT(CASE ... ELSE 0 END) count every row?

COUNT counts non-NULL values, not values equal to 1. With ELSE 0, every row produces a non-NULL number (1 or 0), so COUNT counts all of them.

Drop the ELSE so non-matching rows become NULL, or use SUM(CASE WHEN cond THEN 1 ELSE 0 END).

How do I count paid and pending orders in the same result row?

Give each metric its own conditional aggregate: SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) AS paid, SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending, all in one SELECT.

The table is read once; each aggregate applies its own condition, so several metrics land in one row.

Why did my SUM(CASE ...) come back NULL instead of 0?

When no row matches and there is no ELSE, every CASE result is NULL; SUM over all-NULL is NULL, not 0.

Add ELSE 0 so non-matching rows contribute zero and the metric reads 0 when nothing matches.

Should I use CASE or FILTER for conditional metrics?

FILTER (e.g. COUNT(*) FILTER (WHERE status = 'paid')) is concise and readable, but not supported by every database.

The SUM/COUNT(CASE ...) form is more portable; prefer it for cross-database SQL, and reach for FILTER when your database supports it and portability isn't a concern.

Don’t paste credentials, personal data, or confidential production records.