Module 2: Aggregating & Grouping Data45 min

Conditional Aggregation with CASE WHEN

Progress Tracking

Log in to save this lesson and continue from where you left off.

Log in

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:

SQL
CASE
  WHEN condition1 THEN result1
  WHEN condition2 THEN result2
  ELSE default_result
END

CASE 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:

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

Alternative COUNT syntax

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.

SQL
-- 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:

SQL
SUM(
CASE
  WHEN salary IS NULL THEN NULL -- Exclude from count entirely
  WHEN salary < 80000 THEN 1
  ELSE 0
END
)
SQL
SELECT
  ROUND(
    100.0 * SUM(CASE WHEN salary > 100000 THEN 1 ELSE 0 END)
    / COUNT(*),
    2
  ) AS pct_high_earners
FROM techcorp_workforce;
Avoid integer division

Use 100.0 (not 100) to avoid integer division. In SQL, 3 / 4 = 0, but 3.0 / 4 = 0.75.

Table: airbnb_contacts
id_guestid_hostid_listingts_contact_atts_reply_atts_accepted_atts_booking_atds_checkinds_checkoutn_guestsn_messages
86b39b70-965b-479d-a0b0-719b195acea21dfb22ec-c20e-4bf9-b161-1607afa25c5ad668de42-122a-45cd-b91f-91a70895f9022014-04-18 09:32:232014-04-18 09:39:062014-12-312015-01-0275
14f943bb-74e9-458b-be55-203dc72206883347390d-8670-4870-9dab-da30f370014114c47fb8-e831-4044-9674-9b3fd04991932014-10-06 06:55:452014-10-06 10:06:382014-10-06 10:06:382014-10-06 10:06:382014-11-032014-11-0728
425aa1ed-82ab-4ecf-b62f-d61e1848706d02cafb86-5445-45cc-80f2-405291578356c5a4a913-a094-4a9d-82e2-0b2d4f9d9eeb2014-10-04 05:02:392014-10-04 23:10:012014-11-022014-11-0922
bb490ede-8a70-4d61-a2e8-625855a393e2f49c3095-58de-4b8d-9d5b-3bfceceb47d827f4b429-d544-464f-b4b5-3c09fd5992e72014-08-31 11:46:112014-08-31 16:48:282014-11-032014-11-0725
b2fda15a-89bb-4e6e-ae81-8b21598e248271f1d49e-2ff4-4d72-b8e6-fd4c67feaa7495fb78ca-8e6e-436a-9830-949d995ad14f2014-10-08 15:07:562014-10-08 15:32:122014-10-08 15:32:122014-10-08 22:21:412014-11-062014-11-09210
1
Try Calculating Acceptance Rate

Calculate what percentage of Airbnb contact requests were accepted (ts_accepted_at IS NOT NULL).

Tables: airbnb_contacts

Conditional Sums

You can sum values conditionally, not just count.

Table: techcorp_workforce
idfirst_namelast_namedepartmentsalaryphone_numberjoining_date
1SarahMitchellHR95000555-01012021-03-15
2MichaelChenHR88000555-01022022-06-01
3EmilyRodriguezHR825002021-09-20
4DavidParkHR80000555-01042023-01-10
5LisaThompsonHR650002021-04-05
2
Break Down Payroll by Department

Add an Admin payroll column alongside the existing HR payroll to break down the total by department.

Tables: techcorp_workforce

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:

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

Table: titanic
passengeridsurvivedpclassnamesexagesibspparchticketfarecabinembarked
103Braund, Mr. Owen Harrismale2210A/5 211717.25S
211Cumings, Mrs. John Bradley (Florence Briggs Thayer)female3810PC 1759971.28C85C
313Heikkinen, Miss. Lainafemale2600STON/O2. 31012827.92S
411Futrelle, Mrs. Jacques Heath (Lily May Peel)female351011380353.1C123S
503Allen, Mr. William Henrymale35003734508.05S
3
Titanic Survivors and Non-Survivors
View solution

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.

Tables: titanic

Multiple Conditions

CASE WHEN can handle complex logic.

Table: techcorp_workforce
idfirst_namelast_namedepartmentsalaryphone_numberjoining_date
1SarahMitchellHR95000555-01012021-03-15
2MichaelChenHR88000555-01022022-06-01
3EmilyRodriguezHR825002021-09-20
4DavidParkHR80000555-01042023-01-10
5LisaThompsonHR650002021-04-05
4
Create Salary Buckets

Add the mid-range (75000 to under 100000) and over-100K buckets to create a three-tier salary distribution.

Tables: techcorp_workforce

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.

Table: facebook_complaints
complaint_idtypeprocessed
00TRUE
10TRUE
20FALSE
31TRUE
41TRUE
5
Processed Ticket Rate By Type
View solution

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.

Tables: facebook_complaints

Share of Active Users

Calculate a percentage using SUM(CASE WHEN) divided by COUNT(*). Use 100.0 * to avoid integer division.

Table: fb_active_users
user_idnamestatuscountry
33Amanda LeonopenAustralia
27Jessica FarrellopenLuxembourg
18Wanda RamirezopenUSA
50Samuel MillerclosedBrazil
16Jacob YorkopenAustralia
6
Share of Active Users
View solution

Calculate the percentage of users who are both from the US and have an 'open' status, as indicated in the `fb_active_users` table.

Tables: fb_active_users

Key Takeaways

  • SUM(CASE WHEN condition THEN 1 ELSE 0 END) counts matching rows
  • This lets you get multiple conditional counts without GROUP BY
  • NULL values fall through to ELSE (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.