SQL-Engineering-Handbook

03 · Conditional Aggregation

🏠 Module Home · 🗂️ Handbook Home · ← 02 Multiple Aggregations · Next → 04 ROLLUP, CUBE & GROUPING SETS

Conditional Aggregation

Module: 02 — Advanced Aggregations Domain used in this file: Banking / Finance (accounts, transactions, branches) Companion file: 03_CONDITIONAL_AGGREGATION.sql


Introduction

WHERE filters rows before aggregation — one condition, applied to the whole query. Conditional aggregation puts the condition inside the aggregate function itself, so a single query can compute several differently filtered metrics side by side. This is the technique behind almost every “breakdown by status” report you have ever seen: new vs. returning, deposits vs. withdrawals, on-time vs. late.


Concept Overview

Wrapping a CASE WHEN expression inside COUNT(), SUM(), or AVG() lets each row “opt in” to a specific metric based on a condition, while still being aggregated in the same GROUP BY pass as every other metric in the query.

COUNT(CASE WHEN condition THEN 1 END)          -- counts only matching rows
SUM(CASE WHEN condition THEN amount ELSE 0 END) -- sums only matching rows
AVG(CASE WHEN condition THEN amount END)         -- averages only matching rows

The CASE expression returns NULL for rows that don’t match — and COUNT(), SUM(), and AVG() all ignore NULLs by design, which is exactly the behavior this pattern relies on.


Business Motivation

A branch operations manager wants, per branch, in one row: number of deposit transactions, number of withdrawal transactions, total deposit amount, and total withdrawal amount. Running four separately filtered queries means four table scans and four result sets to merge by hand — and a real risk that the underlying data changes slightly between queries, making the four numbers technically inconsistent. Conditional aggregation computes all four from a single grouped scan, guaranteed consistent.


Why This Feature Exists

GROUP BY alone can only slice a metric by a column already present in the group. It cannot, by itself, produce two differently-filtered metrics inside the same group. CASE inside an aggregate function is the bridge — it lets you define an arbitrary business condition (not just a raw column value) and turn it into its own metric, without changing the grouping or issuing a second query.


Real Company Examples


Business Problems Solved


Visual Explanation

Detail rows (transactions)                Conditional aggregation, grouped by branch
┌─────────┬────────┬────────┐             ┌────────┬───────────┬──────────────┬───────────────┬──────────────────┐
│ branch  │ type   │ amount │             │ branch │ deposits  │ deposit_amt   │ withdrawals    │ withdrawal_amt    │
├─────────┼────────┼────────┤   GROUP BY  ├────────┼───────────┼──────────────┼───────────────┼──────────────────┤
│ B1      │ DEPOSIT│  500   │──┐  branch   │ B1     │ 2         │ 800           │ 1              │ 150                │
│ B1      │ DEPOSIT│  300   │──┤ ────────▶ └────────┴───────────┴──────────────┴───────────────┴──────────────────┘
│ B1      │ WITHDRW│  150   │──┘
└─────────┴────────┴────────┘

deposits and deposit_amt only “count” the rows where type = 'DEPOSIT'; withdrawals and withdrawal_amt only count type = 'WITHDRAWAL' rows — computed together, from the same grouped rows, in one query.


Syntax

SELECT
    group_col,
    COUNT(CASE WHEN condition_a THEN 1 END)                    AS count_a,
    SUM(CASE WHEN condition_a THEN amount_col ELSE 0 END)       AS sum_a,
    AVG(CASE WHEN condition_b THEN amount_col END)               AS avg_b
FROM table_name
GROUP BY group_col;

Note the ELSE difference: COUNT(CASE WHEN ... THEN 1 END) deliberately omits ELSE, so non-matching rows become NULL and are excluded from the count. SUM(CASE WHEN ... THEN amount ELSE 0 END) commonly does include ELSE 0 — mathematically equivalent to omitting it for SUM(), since SUM() also ignores NULL, but explicit ELSE 0 is a common house-style convention for readability.


Detailed Walkthrough

SELECT
    br.branch_name,
    COUNT(CASE WHEN t.transaction_type = 'DEPOSIT' THEN 1 END)     AS deposit_count,
    SUM(CASE WHEN t.transaction_type = 'DEPOSIT'
             THEN t.amount ELSE 0 END)                             AS deposit_total,
    COUNT(CASE WHEN t.transaction_type = 'WITHDRAWAL' THEN 1 END)   AS withdrawal_count,
    SUM(CASE WHEN t.transaction_type = 'WITHDRAWAL'
             THEN t.amount ELSE 0 END)                              AS withdrawal_total
FROM transactions AS t
JOIN accounts      AS a  ON t.account_id = a.account_id
JOIN branches       AS br ON a.branch_id  = br.branch_id
GROUP BY br.branch_name;
  1. The joins produce one row per transaction, tagged with its branch.
  2. GROUP BY br.branch_name sets the grain to one row per branch.
  3. Each CASE-wrapped aggregate independently evaluates every row in the group against its own condition, contributing to only the metric it matches.
  4. All four metrics are guaranteed to come from the exact same underlying transaction rows.

Production Workflow

Conditional aggregation queries like this typically power operational dashboards refreshed intraday (branch operations, fraud monitoring) or nightly (finance close reports), and are frequently the SQL behind a scheduled email report sent to branch managers.


Analytics Engineering Perspective


Performance Considerations


Edge Cases


Common Mistakes


Best Practices


Interview Questions

  1. Why does COUNT(CASE WHEN condition THEN 1 END) correctly count only matching rows, with no ELSE needed? COUNT() ignores NULL values, and the CASE expression returns NULL for any row where the condition is false, since there’s no ELSE branch.
  2. What happens if you add ELSE 0 to a conditional COUNT()? It breaks the pattern — 0 is not NULL, so COUNT() now counts every row regardless of the condition.
  3. Why is ELSE 0 dangerous inside AVG(CASE WHEN ...) but safe inside SUM(CASE WHEN ...)? SUM() treats 0 and NULL (which is ignored) identically for a sum’s result. AVG() divides by the count of included values — including zeros in that count for non-matching rows pulls the average down incorrectly.
  4. How would you build a pivot-style report (one column per status value) without a native PIVOT operator? Conditional aggregation — one CASE-wrapped aggregate per status value, all inside the same GROUP BY query.
  5. What happens to a row whose value doesn’t match any WHEN branch and has no ELSE? The CASE expression evaluates to NULL for that row, and it is excluded from that particular conditional aggregate.

Summary

Conditional aggregation moves a business condition from WHERE (which filters the whole query) into a CASE expression inside an aggregate function (which filters just that one metric). This is the core technique for building pivot-style, multi-status reports in SQL dialects without a native PIVOT, and it depends entirely on correctly understanding how NULL interacts with COUNT(), SUM(), and AVG().


Practice Challenges

  1. Add a third conditional metric to the walkthrough query: count and total amount of 'TRANSFER' transactions per branch.
  2. Explain, without running it, what would go wrong if ELSE 0 were added to the deposit_count expression in the walkthrough.
  3. Build a per-branch report showing average deposit amount and average withdrawal amount side by side, using AVG(CASE WHEN ...) correctly (no ELSE 0).
  4. Design a conditional KPI for “high-value transaction rate” (percentage of transactions over a defined threshold) per branch, using COUNT(CASE WHEN ...) divided by COUNT(*).
  5. Rewrite one of this file’s scenarios in the companion .sql file as a GROUP BY transaction_type query instead, and explain when that alternative shape would be preferable to conditional aggregation.

Further Reading


◀ Previous: 02_MULTIPLE_AGGREGATIONS.md · Next ▶ 04_ROLLUP_CUBE_GROUPING_SETS.md


⬆ Back to top · 🏠 Module Home · 🗂️ Handbook Home