CASE expressions
Hook
You’ve used CASE in Level 2 to figure out whether the Lions were home or
away. It does a lot more. CASE is how you bucket continuous values into
labels, recode categories, or compute conditional aggregates — all without a
join.
Concept
Two forms:
-- Searched form (most flexible)CASE WHEN condition_1 THEN result_1 WHEN condition_2 THEN result_2 ELSE fallbackEND
-- Simple form (when comparing one column to many values)CASE column WHEN 'A' THEN 'Apple' WHEN 'B' THEN 'Banana' ELSE 'Other'ENDThe killer combo: CASE inside SUM or COUNT for conditional aggregates.
SELECT player_display_name, SUM(CASE WHEN receiving_yards >= 100 THEN 1 ELSE 0 END) AS century_games, SUM(CASE WHEN receiving_tds > 0 THEN 1 ELSE 0 END) AS td_gamesFROM weekly_statsWHERE recent_team = 'DET' AND season = 2024GROUP BY player_display_name;That’s two metrics from one scan of the table. Much faster than two queries.
Lions example
Classify every Lions 2024 regular-season game by margin: blowout (15+ point win), close W (1-14 pt win), close L (1-14 pt loss), blowout L (15+ pt loss):
SELECT week, CASE WHEN home_team = 'DET' THEN away_team ELSE home_team END AS opp, CASE WHEN home_team = 'DET' THEN home_score ELSE away_score END AS det, CASE WHEN home_team = 'DET' THEN away_score ELSE home_score END AS opp_pts, CASE WHEN (CASE WHEN home_team = 'DET' THEN home_score ELSE away_score END) - (CASE WHEN home_team = 'DET' THEN away_score ELSE home_score END) >= 15 THEN 'blowout W' WHEN (CASE WHEN home_team = 'DET' THEN home_score ELSE away_score END) > (CASE WHEN home_team = 'DET' THEN away_score ELSE home_score END) THEN 'close W' WHEN (CASE WHEN home_team = 'DET' THEN away_score ELSE home_score END) - (CASE WHEN home_team = 'DET' THEN home_score ELSE away_score END) >= 15 THEN 'blowout L' ELSE 'close L' END AS bucketFROM schedulesWHERE season = 2024 AND game_type = 'REG' AND (home_team = 'DET' OR away_team = 'DET')ORDER BY week;That’s verbose because the home/away logic is everywhere — a perfect use case
for putting the score logic into a CTE first, then doing the CASE bucket
once. Combining CTEs with CASE is how mature SQL gets readable.
Try it
Refactor the example above using a CTE that computes det_score and
opp_score once, then a SELECT that uses a single CASE to bucket margin.
The output should be the same, but the query should be half as long.
Common mistakes
- Forgetting
ELSE. Without it, a row that matches noWHENreturnsNULL. Sometimes that’s correct; sometimes it silently breaks downstream aggregations. - Order matters.
CASEevaluatesWHENclauses in order and stops at the first match. Put more specific conditions before more general ones. CASEfor things you should split out. If a singleCASEhas eight branches and four different output types, you probably want a CTE or a helper table, not a longerCASE.CASEinsideWHEREinstead of separate conditions.WHERE CASE WHEN x THEN y END = zis usually less readable thanWHERE (x AND y = z) OR (NOT x AND ...).
Quick check
- What’s the difference between the simple form and the searched form?
- What does a
CASEreturn when noWHENmatches and there’s noELSE? - Why is
SUM(CASE WHEN x THEN 1 ELSE 0 END)faster than running a separateCOUNT(*)query withWHERE x?