Why conditional aggregates beat self-joins

A common report asks for revenue by segment in columns: West, East, Online, Store. The naive way is four separate queries joined together, which is slow and noisy. A conditional aggregate computes all four columns in one pass by embedding a CASE WHEN inside the SUM. On the Orders table this turns ten lines of SQL into three and runs against the data once.

The trick is simple: the aggregate only counts rows where the inner condition is true, and the ELSE makes the rest contribute zero. Once that clicks, most pivot reports become one query.

Conditional SUM with CASE WHEN

SELECT SUM(CASE WHEN region = 'West' THEN amount ELSE 0 END) AS west_rev,
  SUM(CASE WHEN region = 'East' THEN amount ELSE 0 END) AS east_rev
FROM Orders
1
Sum one segment in a column

Each column adds only its matching rows.

SELECT SUM(CASE WHEN region = 'West' THEN amount ELSE 0 END) AS west_rev
FROM Orders
WHERE order_date >= '2025-01-01'
2
Add a date filter outside

The WHERE trims the rows first, then the conditional sum runs on the remainder.

SELECT SUM(amount) FILTER (
  WHERE region = 'West') AS west_rev,
  SUM(amount) FILTER (
  WHERE region = 'East') AS east_rev
FROM Orders
3
Use FILTER in Postgres

The FILTER clause is cleaner than CASE and reads like plain English on Postgres and newer BigQuery.

Query editor showing conditional SUM columns for West, East, and Online revenue side by side.

Conditional COUNT and AVG

SELECT COUNT(CASE WHEN status = 'late' THEN 1 END) AS late_orders
FROM Orders
4
Count matching rows

Note there is no ELSE, so non-late rows return NULL and COUNT skips them. This counts only late orders in one column.

SELECT AVG(CASE WHEN region = 'West' THEN amount END) AS west_avg
FROM Orders
5
Average a subset

The AVG ignores NULLs, so only West amounts enter the average. This is cleaner than a separate WHERE region = 'West' subquery.

SELECT COUNT(DISTINCT CASE WHEN region = 'West' THEN customer_id END) AS west_customers
FROM Orders
6
Count distinct in a segment

counts unique West buyers without a subquery.

Pro Tip

I recommend AVG(CASE WHEN ... THEN amount END) over SUM(...)/COUNT(...) because the divide is handled for you and ignores NULLs automatically. A common mistake is using COUNT(CASE WHEN ... THEN 1 ELSE 0 END), which counts every row since 0 is not null.

Building a pivot table with one query

SELECT product_id,
  SUM(CASE WHEN region = 'West' THEN amount ELSE 0 END) AS west,
  SUM(CASE WHEN region = 'East' THEN amount ELSE 0 END) AS east
FROM Orders
GROUP BY product_id
7
Put segments in columns

The result is a ready pivot: one row per product, regions as columns.

SELECT product_id,
  SUM(amount) AS total, SUM(CASE WHEN region = 'West' THEN amount ELSE 0 END) AS west
FROM Orders
GROUP BY product_id
8
Add a total column

The plain SUM gives the grand total alongside the conditional one.

SELECT product_id,
  SUM(CASE WHEN region = 'West' THEN amount ELSE 0 END) * 1.0 / SUM(amount)
  AS west_share
FROM Orders
GROUP BY product_id
9
Compute a share

Multiply by 1.0 first so the division uses decimals, not integer truncation.

COUNT DISTINCT and window aggregates

SELECT region, COUNT(DISTINCT customer_id) AS buyers
FROM Orders
GROUP BY region
10
Count unique buyers per region

DISTINCT inside COUNT removes repeat visits so you report people, not orders.

SELECT order_date, SUM(amount) OVER (
  ORDER BY order_date) AS running_total
FROM Orders
11
Running total with a window

The window aggregate keeps every row and adds a cumulative sum, covered more in our SQL window functions and CTEs guide.

The divide-by-zero trap

When you divide one aggregate by another, a group with zero rows makes the denominator zero and the whole expression errors or returns NULL. This is the most common failure in a share or ratio report, and it often shows up only for a quiet segment that has no data yet.

SELECT product_id,
  CASE WHEN SUM(amount)
  = 0 THEN 0
  ELSE SUM(CASE WHEN region='West' THEN amount ELSE 0 END) * 1.0 / SUM(amount)
  END AS west_share
FROM Orders
GROUP BY product_id
12
Guard the denominator

The CASE returns 0 instead of dividing by zero.

13
Use NULLIF in Postgres and SQL Server

SELECT west / NULLIF(total, 0) AS share FROM (...) t replaces a zero denominator with NULL, which displays as blank instead of crashing the query.

Pro Tip

This will break if you ship a ratio report to a dashboard and one new segment has no rows yet: the divide-by-zero kills the whole tile. In my experience wrapping the denominator in NULLIF on day one saves a 2 a.m. page later.

Worked example: regional revenue and share in one query

Here is a runnable query against Orders that returns each product's total, West revenue, and West share, all computed in a single pass.

SELECT product_id,
  SUM(amount) AS total,
  SUM(CASE WHEN region = 'West' THEN amount ELSE 0 END) AS west_rev,
  CASE WHEN SUM(amount)
  = 0 THEN 0
  ELSE SUM(CASE WHEN region = 'West' THEN amount ELSE 0 END) * 1.0 / SUM(amount)
  END AS west_share
FROM Orders
GROUP BY product_id
ORDER BY total DESC;
14
Read a sample row

A row with product_id 48213, total 120000, west_rev 54000, west_share 0.45 means that product earned $120,000 overall and 45 percent of it came from the West. One query, four metrics, no joins.

Result grid showing product_id with total, west_rev, and west_share columns.

Conditional aggregates inside window functions

You can combine CASE with a window function to compute a conditional running total. SUM(CASE WHEN region = 'West' THEN amount ELSE 0 END) OVER (ORDER BY order_date) gives a running West revenue that keeps every row, so you can plot the segment's cumulative line without a self-join or a temp table.

The window version keeps the detail rows, while a plain GROUP BY collapses them into one summary row. A common mistake is forgetting the OVER clause, which turns the expression back into a single grand total for the whole table and loses the per-row line entirely. Always pair the conditional aggregate with an explicit window so the running calculation is clear.

SELECT order_date,
  SUM(CASE WHEN region = 'West' THEN amount ELSE 0 END) OVER (
  ORDER BY order_date) AS running_west
FROM Orders
ORDER BY order_date
15
Running West revenue per day

Each row shows the West revenue accumulated up to that day.

Computing several metrics in one pass

A single SELECT can return the total, the West share, the East share, and the average ticket at once, which is far cheaper than running four separate queries against the Orders table. Stakeholders rarely want one number; they want the full scorecard, and conditional aggregates deliver it in a single scan of the data.

In my experience one combined query also prevents mismatched row counts between metrics, which happens when you run separate queries that each filter the data differently. Keep every metric in the same GROUP BY so the columns line up row for row. See our GROUP BY and HAVING guide for the grouping basics this pattern builds directly on.

SELECT product_id,
  SUM(amount) AS total,
  AVG(amount) AS avg_ticket,
  SUM(CASE WHEN region = 'West' THEN amount ELSE 0 END) * 1.0 / SUM(amount)
  AS west_share
FROM Orders
GROUP BY product_id
ORDER BY total DESC
16
Scorecard query per product

One pass returns the whole product scorecard.

Scorecard grid with total, average ticket, and West share columns per product.

Before you ship a conditional aggregate report, confirm the column totals add up to the grand total. A West share of 0.45 should pair with an East and Other share that sum to 1.0, and a missing ELSE in a COUNT can quietly drop rows so the segments no longer reconcile. I keep a final SELECT that sums the segment columns and compares them to the plain total, and I fail the query if they differ by more than a rounding cent. In my experience that one reconciliation step catches more logic errors than any code review, because the numbers are right in front of you and the mismatch is impossible to ignore once it is printed.

Next steps and a quick checklist

Use CASE WHEN inside SUM, COUNT, and AVG to build pivot columns in one query. Prefer AVG(CASE ...) over manual divides, multiply by 1.0 before a ratio, and guard every denominator with NULLIF or a CASE. Combine these with GROUP BY for the row dimension and with date aggregation when the segments are time periods. Practice on the Orders table until a five-query report collapses into a single clean SELECT. These column patterns sit on top of SQL GROUP BY and HAVING, and pair well with SQL date aggregation when the segments are time periods. Add SQL CASE WHEN labels before you sum to keep the logic readable.