pbPassingBI
/
Aggregation & summary intermediate 6 min

Conditional aggregation with CASE

Pivoting inside a single query with CASE inside an aggregate.

What you'll be able to do
  • Write CASE expressions
  • Count and sum conditionally
  • Replace multiple queries with one pass

CASE basics

CASE WHEN total > 500 THEN 'large'
     WHEN total > 100 THEN 'medium'
     ELSE 'small'
END

Conditions are evaluated top to bottom and the first match wins, so order matters. Without ELSE, unmatched rows return null.

CASE inside an aggregate

This is the pattern worth memorising:

SELECT region,
       COUNT(*) AS all_orders,
       COUNT(CASE WHEN status = 'shipped' THEN 1 END)      AS shipped,
       SUM(CASE WHEN status = 'refunded' THEN total ELSE 0 END) AS refunded_value
FROM orders
GROUP BY region;

Note the missing ELSE in the COUNT. Unmatched rows return null, and COUNT skips nulls — which is exactly what makes it count only the matches. Adding ELSE 0 there would count everything, since zero is not null.

Pivoting rows into columns

The same trick turns row values into columns:

SELECT product,
       SUM(CASE WHEN year = 2023 THEN revenue ELSE 0 END) AS rev_2023,
       SUM(CASE WHEN year = 2024 THEN revenue ELSE 0 END) AS rev_2024
FROM sales
GROUP BY product;

One pass, one query, instead of two queries joined together.

The FILTER alternative

PostgreSQL and SQLite offer a clearer syntax for the same thing:

COUNT(*) FILTER (WHERE status = 'shipped')

Use it where available; CASE is the portable fallback.

Key points
  • CASE conditions evaluate top to bottom; first match wins
  • COUNT(CASE WHEN ... THEN 1 END) with no ELSE counts only matches
  • Conditional aggregation pivots data in a single pass
Check yourself