Conditional Aggregation with CASE WHEN
Progress Tracking
Log in to save this lesson and continue from where you left off.
When Basic Aggregates Aren’t Enough
So far, your aggregates operate on all rows (or all rows in a group). But what if you need to count only certain values? Sum only specific items? Get multiple conditional counts in one query?
You could run separate queries with different WHERE clauses. But there’s a better way: put CASE WHEN inside your aggregate function.
Quick CASE Refresher
Before we dive in, here’s the CASE syntax:
CASE
WHEN condition1 THEN result1
WHEN condition2 THEN result2
ELSE default_result
ENDCASE evaluates conditions in order and returns the result for the first true condition. If none match, it returns the ELSE value (or NULL if there’s no ELSE).
The Pattern: SUM(CASE WHEN…)
Here’s the most common pattern. It counts or sums values that meet a condition:
SELECT
COUNT(*) AS total_employees,
SUM(CASE WHEN department = 'HR' THEN 1 ELSE 0 END) AS hr_count,
SUM(CASE WHEN department = 'Admin' THEN 1 ELSE 0 END) AS admin_count,
SUM(CASE WHEN department = 'Account' THEN 1 ELSE 0 END) AS account_count
FROM techcorp_workforce;One row back with four columns: total, HR count, Admin count, Account count. No GROUP BY needed.
How It Works
For each row, CASE WHEN checks the condition. If true, it returns 1. If false, it returns 0. SUM adds up all those 1s and 0s. The result: a count of matching rows.
You can also use COUNT(CASE WHEN condition THEN 1 END) without the ELSE. COUNT ignores NULLs, so it only counts the 1s. Same result, slightly different approach.
Watch Out for NULLs
If a column value is NULL, comparisons like salary < 80000 are neither true nor false. The CASE falls through to the ELSE.
-- If salary is NULL, this returns 0 (the ELSE)
SUM(
CASE
WHEN salary < 80000 THEN 1
ELSE 0
END
)This effectively treats unknown salaries as “not matching.” That’s usually fine, but be aware of it. If you need to explicitly handle NULLs, add a condition:
SUM(
CASE
WHEN salary IS NULL THEN NULL -- Exclude from count entirely
WHEN salary < 80000 THEN 1
ELSE 0
END
)SELECT
ROUND(
100.0 * SUM(CASE WHEN salary > 100000 THEN 1 ELSE 0 END)
/ COUNT(*),
2
) AS pct_high_earners
FROM techcorp_workforce;Use 100.0 (not 100) to avoid integer division. In SQL, 3 / 4 = 0, but 3.0 / 4 = 0.75.
| id_guest | id_host | id_listing | ts_contact_at | ts_reply_at | ts_accepted_at | ts_booking_at | ds_checkin | ds_checkout | n_guests | n_messages |
|---|---|---|---|---|---|---|---|---|---|---|
| 86b39b70-965b-479d-a0b0-719b195acea2 | 1dfb22ec-c20e-4bf9-b161-1607afa25c5a | d668de42-122a-45cd-b91f-91a70895f902 | 2014-04-18 09:32:23 | 2014-04-18 09:39:06 | 2014-12-31 | 2015-01-02 | 7 | 5 | ||
| 14f943bb-74e9-458b-be55-203dc7220688 | 3347390d-8670-4870-9dab-da30f3700141 | 14c47fb8-e831-4044-9674-9b3fd0499193 | 2014-10-06 06:55:45 | 2014-10-06 10:06:38 | 2014-10-06 10:06:38 | 2014-10-06 10:06:38 | 2014-11-03 | 2014-11-07 | 2 | 8 |
| 425aa1ed-82ab-4ecf-b62f-d61e1848706d | 02cafb86-5445-45cc-80f2-405291578356 | c5a4a913-a094-4a9d-82e2-0b2d4f9d9eeb | 2014-10-04 05:02:39 | 2014-10-04 23:10:01 | 2014-11-02 | 2014-11-09 | 2 | 2 | ||
| bb490ede-8a70-4d61-a2e8-625855a393e2 | f49c3095-58de-4b8d-9d5b-3bfceceb47d8 | 27f4b429-d544-464f-b4b5-3c09fd5992e7 | 2014-08-31 11:46:11 | 2014-08-31 16:48:28 | 2014-11-03 | 2014-11-07 | 2 | 5 | ||
| b2fda15a-89bb-4e6e-ae81-8b21598e2482 | 71f1d49e-2ff4-4d72-b8e6-fd4c67feaa74 | 95fb78ca-8e6e-436a-9830-949d995ad14f | 2014-10-08 15:07:56 | 2014-10-08 15:32:12 | 2014-10-08 15:32:12 | 2014-10-08 22:21:41 | 2014-11-06 | 2014-11-09 | 2 | 10 |
Calculate what percentage of Airbnb contact requests were accepted (ts_accepted_at IS NOT NULL).
Conditional Sums
You can sum values conditionally, not just count.
| id | first_name | last_name | department | salary | phone_number | joining_date |
|---|---|---|---|---|---|---|
| 1 | Sarah | Mitchell | HR | 95000 | 555-0101 | 2021-03-15 |
| 2 | Michael | Chen | HR | 88000 | 555-0102 | 2022-06-01 |
| 3 | Emily | Rodriguez | HR | 82500 | 2021-09-20 | |
| 4 | David | Park | HR | 80000 | 555-0104 | 2023-01-10 |
| 5 | Lisa | Thompson | HR | 65000 | 2021-04-05 |
Add an Admin payroll column alongside the existing HR payroll to break down the total by department.
Now you get payroll broken down by department, all in one row.
Creating Pivot Tables
Here’s where this gets really useful. You can create pivot-style cross-tabulations:
SELECT
survived,
SUM(CASE WHEN pclass = 1 THEN 1 ELSE 0 END) AS first_class,
SUM(CASE WHEN pclass = 2 THEN 1 ELSE 0 END) AS second_class,
SUM(CASE WHEN pclass = 3 THEN 1 ELSE 0 END) AS third_class
FROM titanic
GROUP BY survived;This creates a cross-tabulation: survival status on the rows, passenger class on the columns. One query, and you’ve got a pivot table.
Titanic Survivors and Non-Survivors
Use SUM(CASE WHEN pclass = X THEN 1 ELSE 0 END) for each class, grouped by survived status.
| passengerid | survived | pclass | name | sex | age | sibsp | parch | ticket | fare | cabin | embarked |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 0 | 3 | Braund, Mr. Owen Harris | male | 22 | 1 | 0 | A/5 21171 | 7.25 | S | |
| 2 | 1 | 1 | Cumings, Mrs. John Bradley (Florence Briggs Thayer) | female | 38 | 1 | 0 | PC 17599 | 71.28 | C85 | C |
| 3 | 1 | 3 | Heikkinen, Miss. Laina | female | 26 | 0 | 0 | STON/O2. 3101282 | 7.92 | S | |
| 4 | 1 | 1 | Futrelle, Mrs. Jacques Heath (Lily May Peel) | female | 35 | 1 | 0 | 113803 | 53.1 | C123 | S |
| 5 | 0 | 3 | Allen, Mr. William Henry | male | 35 | 0 | 0 | 373450 | 8.05 | S |
Make a report showing the number of survivors and non-survivors by passenger class. Classes are categorized based on the `pclass` value as: • First class: `pclass = 1` • Second class: `pclass = 2` • Third class: `pclass = 3` Output one row per survival status, with a column for the number of passengers in each class.
Multiple Conditions
CASE WHEN can handle complex logic.
| id | first_name | last_name | department | salary | phone_number | joining_date |
|---|---|---|---|---|---|---|
| 1 | Sarah | Mitchell | HR | 95000 | 555-0101 | 2021-03-15 |
| 2 | Michael | Chen | HR | 88000 | 555-0102 | 2022-06-01 |
| 3 | Emily | Rodriguez | HR | 82500 | 2021-09-20 | |
| 4 | David | Park | HR | 80000 | 555-0104 | 2023-01-10 |
| 5 | Lisa | Thompson | HR | 65000 | 2021-04-05 |
Add the mid-range (75000 to under 100000) and over-100K buckets to create a three-tier salary distribution.
Processed Ticket Rate By Type
For each type, divide SUM(CASE WHEN processed THEN 1 ELSE 0 END) by COUNT(*). The processed column is boolean, so it works as a condition on its own. Multiply by 1.0 to avoid integer division, and round the rate to two decimal places.
| complaint_id | type | processed |
|---|---|---|
| 0 | 0 | TRUE |
| 1 | 0 | TRUE |
| 2 | 0 | FALSE |
| 3 | 1 | TRUE |
| 4 | 1 | TRUE |
Find the processed rate of tickets for each `type`. The processed rate is defined as the number of processed tickets divided by the total number of tickets for that type. Round this result to two decimal places.
Share of Active Users
Calculate a percentage using SUM(CASE WHEN) divided by COUNT(*). Use 100.0 * to avoid integer division.
| user_id | name | status | country |
|---|---|---|---|
| 33 | Amanda Leon | open | Australia |
| 27 | Jessica Farrell | open | Luxembourg |
| 18 | Wanda Ramirez | open | USA |
| 50 | Samuel Miller | closed | Brazil |
| 16 | Jacob York | open | Australia |
Calculate the percentage of users who are both from the US and have an 'open' status, as indicated in the `fb_active_users` table.
Key Takeaways
SUM(CASE WHEN condition THEN 1 ELSE 0 END)counts matching rows- This lets you get multiple conditional counts without
GROUP BY NULLvalues fall through toELSE(treated as non-matching)- Use it for percentages: conditional count divided by total count
- Use 100.0 for percentages to avoid integer division
- You can sum values conditionally, not just count
- Combined with
GROUP BY, you can create pivot-style reports
What’s Next
You’ve now got all the building blocks. The final lesson pulls everything together with complex queries that combine filtering, grouping, and conditional aggregation.