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 the number of survivors and non-survivors by each class.

Tables: titanic

Multiple Conditions

CASE WHEN can handle complex logic.

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

Inspections by Risk Category

Use SUM(CASE WHEN risk_category = ... THEN 1 ELSE 0 END) for each category. Remember to handle the NULL risk category as a separate group.

Table: sf_restaurant_health_violations
business_idbusiness_namebusiness_addressbusiness_citybusiness_statebusiness_postal_codebusiness_latitudebusiness_longitudebusiness_locationbusiness_phone_numberinspection_idinspection_dateinspection_scoreinspection_typeviolation_idviolation_descriptionrisk_category
5800John Chin Elementary School350 Broadway StSan FranciscoCA9413337.8-122.4{'longitude': '-122.403154', 'needs_recoding': False, 'latitude': '37.798358', 'human_address': '{"address":"","city":"","state":"","zip":""}'}5800_201710172017-10-1798Routine - Unscheduled5800_20171017_103149Wiping cloths not clean or properly stored or inadequate sanitizerLow Risk
64236Sutter Pub and Restaurant700 Sutter StSan FranciscoCA9410237.79-122.41{'longitude': '-122.41188', 'needs_recoding': False, 'latitude': '37.78881', 'human_address': '{"address":"","city":"","state":"","zip":""}'}64236_201707252017-07-2588Routine - Unscheduled64236_20170725_103133Foods not protected from contaminationModerate Risk
1991SRI THAI CUISINE4621 LINCOLN WaySan FranciscoCA9412237.76-122.51{'longitude': '-122.507779', 'needs_recoding': False, 'latitude': '37.764073', 'human_address': '{"address":"","city":"","state":"","zip":""}'}1991_201711292017-11-2986Routine - Unscheduled1991_20171129_103139Improper food storageLow Risk
3816Washington Bakery & Restaurant733 Washington StSan FranciscoCA9410837.8-122.41{'longitude': '-122.405845', 'needs_recoding': False, 'latitude': '37.795174', 'human_address': '{"address":"","city":"","state":"","zip":""}'}3816_201607282016-07-2867Routine - Unscheduled3816_20160728_103108Contaminated or adulterated foodHigh Risk
39119Brothers Restaurant4128 GEARY BlvdSan FranciscoCA9411837.78-122.46{'longitude': '-122.463762', 'needs_recoding': False, 'latitude': '37.781148', 'human_address': '{"address":"","city":"","state":"","zip":""}'}39119_201607182016-07-1879Routine - Unscheduled39119_20160718_103133Foods not protected from contaminationModerate Risk
5
Find the number of inspections for each risk category by inspection type
View solution

Find the number of inspections that resulted in each risk category per each inspection type. Consider the records with no risk category value belongs to a separate category. Output the result along with the corresponding inspection type and the corresponding total number of inspections per that type. The output should be pivoted, meaning that each risk category + total number should be a separate column. Order the result based on the number of inspections per inspection type in descending order.

Tables: sf_restaurant_health_violations

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.