CASE Statement in SQL
Here's the thing about CASE WHEN. You're going to write it more than almost any other SQL construct in your first year as a data engineer, and nobody ever sits you down and teaches it properly. You pick up the syntax in pieces, you get something working, and then an interviewer asks you to bucket revenue into five tiers under a three minute timer and your brain locks. That's what this page is for.
Know CASE statements the way the interviewer who asks it knows it.
Simple CASE Syntax
-- Simple CASE: compare one expression to fixed values
SELECT
order_id,
status,
CASE status
WHEN 'Pending' THEN 'Awaiting Processing'
WHEN 'Shipped' THEN 'In Transit'
WHEN 'Completed' THEN 'Complete'
WHEN 'Returned' THEN 'Returned to Warehouse'
ELSE 'Unknown'
END AS status_label
FROM orders;When to Use Simple CASE
-- Custom sort order using simple CASE
SELECT order_id, status
FROM orders
ORDER BY
CASE status
WHEN 'Pending' THEN 1
WHEN 'Processing' THEN 2
WHEN 'Shipped' THEN 3
WHEN 'Completed' THEN 4
ELSE 5
END;Searched CASE Syntax
-- Searched CASE: each WHEN is an independent condition
SELECT
employee_id,
emp_name,
salary,
CASE
WHEN salary >= 150000 THEN 'Executive'
WHEN salary >= 100000 THEN 'Senior'
WHEN salary >= 70000 THEN 'Mid-Level'
WHEN salary >= 40000 THEN 'Junior'
ELSE 'Entry'
END AS salary_band
FROM employees;Multi-Column Conditions
-- Classify employees by both salary and tenure
SELECT
employee_id,
salary,
CAST((julianday('now') - julianday(hire_date)) / 30 AS INT) AS months_active,
CASE
WHEN salary >= 120000 AND (julianday('now') - julianday(hire_date)) / 30 >= 24
THEN 'Senior Leader'
WHEN salary >= 80000 OR (julianday('now') - julianday(hire_date)) / 30 >= 12
THEN 'Established'
WHEN (julianday('now') - julianday(hire_date)) / 30 >= 3
THEN 'Growing'
ELSE 'New'
END AS employee_tier
FROM employees;NULL Handling in CASE
-- NULL-safe CASE
SELECT
content_id,
publish_date,
CASE
WHEN publish_date IS NULL THEN 'Not Published'
WHEN publish_date > date('now', '-7 days')
THEN 'Recently Published'
ELSE 'Published'
END AS publish_status
FROM content_items;
-- WRONG: this never matches NULL
-- CASE publish_date WHEN NULL THEN 'Not Published' END
-- NULL = NULL evaluates to NULL (not TRUE), so it skipsCASE with Aggregation: Conditional Counting and Summing
-- Conditional count: orders by status, per region, in one query
SELECT
region,
COUNT(*) AS total_orders,
COUNT(CASE WHEN status = 'Completed' THEN 1 END) AS completed,
COUNT(CASE WHEN status = 'Returned' THEN 1 END) AS returned,
COUNT(CASE WHEN status = 'Pending' THEN 1 END) AS pending
FROM orders
GROUP BY region
ORDER BY region;-- Conditional sum: profit by region in one query
SELECT
status,
SUM(CASE WHEN region = 'US' THEN profit ELSE 0 END) AS us_profit,
SUM(CASE WHEN region = 'EU' THEN profit ELSE 0 END) AS eu_profit,
SUM(CASE WHEN region = 'APAC' THEN profit ELSE 0 END) AS apac_profit,
SUM(CASE WHEN region = 'LATAM' THEN profit ELSE 0 END) AS latam_profit,
SUM(profit) AS total_profit
FROM orders
GROUP BY status;Top Region by Order Volume
> Which single region generates the most orders? Return the region and its order count.
CASE with AVG and Ratios
-- Completion rate by region
SELECT
region,
ROUND(
100.0
* COUNT(CASE WHEN status = 'Completed' THEN 1 END)
/ NULLIF(COUNT(*), 0),
1
) AS completion_rate_pct
FROM orders
GROUP BY region
ORDER BY region;CASE with Window Functions
-- Running total of bulk-order revenue only (quantity > 1)
SELECT
transaction_id,
transaction_date,
quantity,
total_amount,
SUM(CASE WHEN quantity > 1 THEN total_amount ELSE 0 END)
OVER (ORDER BY transaction_date) AS running_bulk_revenue
FROM transactions
ORDER BY transaction_date;-- Flag rows where an employee's salary is above department average
SELECT
employee_id,
department,
salary,
AVG(salary) OVER (PARTITION BY department) AS dept_avg,
CASE
WHEN salary > AVG(salary) OVER (PARTITION BY department)
THEN 'Above Average'
WHEN salary = AVG(salary) OVER (PARTITION BY department)
THEN 'At Average'
ELSE 'Below Average'
END AS salary_position
FROM employees;-- Classify change direction from previous row
SELECT
transaction_date,
total_amount,
LAG(total_amount) OVER (ORDER BY transaction_date) AS prev_value,
CASE
WHEN total_amount > LAG(total_amount) OVER (ORDER BY transaction_date)
THEN 'Increase'
WHEN total_amount < LAG(total_amount) OVER (ORDER BY transaction_date)
THEN 'Decrease'
WHEN total_amount = LAG(total_amount) OVER (ORDER BY transaction_date)
THEN 'No Change'
ELSE 'N/A' -- first row has no previous value
END AS direction
FROM transactions;CASE in Data Engineering Pipelines
Data Cleaning: Standardize Messy Input
-- Standardize country names from raw data
SELECT
customer_id,
CASE
WHEN UPPER(TRIM(country)) IN ('US', 'USA', 'UNITED STATES', 'U.S.A.')
THEN 'United States'
WHEN UPPER(TRIM(country)) IN ('UK', 'GB', 'UNITED KINGDOM', 'GREAT BRITAIN')
THEN 'United Kingdom'
WHEN UPPER(TRIM(country)) IN ('CA', 'CAN', 'CANADA')
THEN 'Canada'
ELSE TRIM(country)
END AS country_standardized
FROM customers;SCD Type 2 Flag
-- Mark current vs historical records
SELECT
token_id,
scope,
owner_id,
issued AS valid_from,
expires AS valid_to,
CASE
WHEN expires IS NULL OR expires > datetime('now')
THEN 1
ELSE 0
END AS is_current
FROM api_tokens;Data Quality Scoring
-- Score row completeness for a data quality dashboard
SELECT
user_id,
(
CASE WHEN email IS NOT NULL AND email != '' THEN 1 ELSE 0 END
+ CASE WHEN age_bucket IS NOT NULL AND age_bucket != '' THEN 1 ELSE 0 END
+ CASE WHEN account_status IS NOT NULL AND account_status != '' THEN 1 ELSE 0 END
+ CASE WHEN username IS NOT NULL AND username != '' THEN 1 ELSE 0 END
) AS completeness_score,
CASE
WHEN (
CASE WHEN email IS NOT NULL AND email != '' THEN 1 ELSE 0 END
+ CASE WHEN age_bucket IS NOT NULL AND age_bucket != '' THEN 1 ELSE 0 END
+ CASE WHEN account_status IS NOT NULL AND account_status != '' THEN 1 ELSE 0 END
+ CASE WHEN username IS NOT NULL AND username != '' THEN 1 ELSE 0 END
) = 4 THEN 'Complete'
WHEN (
CASE WHEN email IS NOT NULL AND email != '' THEN 1 ELSE 0 END
+ CASE WHEN age_bucket IS NOT NULL AND age_bucket != '' THEN 1 ELSE 0 END
+ CASE WHEN account_status IS NOT NULL AND account_status != '' THEN 1 ELSE 0 END
+ CASE WHEN username IS NOT NULL AND username != '' THEN 1 ELSE 0 END
) >= 2 THEN 'Partial'
ELSE 'Poor'
END AS quality_tier
FROM users;CASE Statement Patterns for Interviews
Pattern 1: Bucketing Continuous Values
"Group users by age bracket" or "categorize orders by amount." Use searched CASE with range conditions. List ranges from largest to smallest (or smallest to largest) to avoid overlaps.
SELECT
CASE
WHEN price >= 500 THEN '500+'
WHEN price >= 200 THEN '200-499'
WHEN price >= 50 THEN '50-199'
WHEN price >= 10 THEN '10-49'
ELSE 'Under 10'
END AS price_band,
COUNT(*) AS product_count
FROM products
GROUP BY 1
ORDER BY MIN(price);Pattern 2: Conditional Aggregation (Pivot)
"Show each department's headcount broken out by gender." Use CASE inside COUNT or SUM to create columns from row values.
SELECT
department,
COUNT(CASE WHEN salary >= 120000 THEN 1 END) AS senior_count,
COUNT(CASE WHEN salary BETWEEN 70000 AND 119999 THEN 1 END) AS mid_count,
COUNT(CASE WHEN salary < 70000 THEN 1 END) AS junior_count,
COUNT(*) AS total
FROM employees
GROUP BY department;Pattern 3: Decode Boolean Flags
Converting 0/1, true/false, or Y/N into readable labels for reporting.
SELECT
product_id,
CASE in_stock
WHEN 1 THEN 'In Stock'
WHEN 0 THEN 'Out of Stock'
END AS stock_status,
CASE WHEN rating >= 4 THEN 'Top Rated'
WHEN rating < 4 THEN 'Standard'
END AS rating_status
FROM products;Pattern 4: Safe Division
Prevent division-by-zero errors in ratio calculations.
SELECT
pipe_id,
rows_in,
rows_out,
CASE
WHEN rows_in = 0 THEN 0
ELSE ROUND(100.0 * rows_out / rows_in, 2)
END AS throughput_rate
FROM data_pipes;Common CASE Statement Mistakes
Overlapping Conditions
Because CASE stops at the first match, overlapping ranges silently produce wrong results.
-- BUG: salary of 120000 matches first condition
CASE
WHEN salary > 50000 THEN 'Mid' -- 120000 > 50000 is TRUE
WHEN salary > 100000 THEN 'Senior' -- never reached for 120000
END
-- FIX: order from most restrictive to least
CASE
WHEN salary > 100000 THEN 'Senior'
WHEN salary > 50000 THEN 'Mid'
ELSE 'Junior'
ENDMissing ELSE Returns NULL
Forgetting ELSE when you do not want NULL results.
-- BUG: status 'canceled' returns NULL
SELECT CASE status
WHEN 'active' THEN 'Active'
WHEN 'inactive' THEN 'Inactive'
END AS label -- 'canceled' -> NULL
-- FIX: always handle the default
SELECT CASE status
WHEN 'active' THEN 'Active'
WHEN 'inactive' THEN 'Inactive'
ELSE 'Other'
END AS labelMixing Data Types in THEN Clauses
All THEN and ELSE values must be the same data type (or implicitly castable). Mixing strings and numbers causes errors in strict-type databases.
-- BUG in strict-type engines: mixing int and text
CASE WHEN quantity > 0 THEN quantity ELSE 'N/A' END
-- FIX: cast to consistent type
CASE WHEN quantity > 0 THEN CAST(quantity AS TEXT) ELSE 'N/A' ENDFrequently asked questions
What is the CASE statement in SQL?+
What is the difference between simple CASE and searched CASE?+
Can you use CASE in a WHERE clause?+
What happens if no CASE condition matches and there is no ELSE?+
You'll be fluent by next Tuesday
- 01
Active recall beats re-reading by 50%
Cognitive-science meta-reviews (Dunlosky et al., 2013) rank practice testing as a top-tier study technique, while re-reading and highlighting rank near the bottom
- 02
76% of hiring managers reject on the coding task, not the resume
From HackerRank's 2024 Developer Skills Report. Candidates who look strong on paper still fail the live screen if they haven't done timed, executable practice
- 03
Five problem shapes cover 80% of data engineer loops
Dedup, sessionization, top-N-per-group, slowly-changing dimensions, partition tricks. Writing the shapes by hand turns the unfamiliar into pattern recognition
More reading
Full CASE WHEN reference with advanced patterns and performance considerations
Hands-on CASE statement problems with real SQL execution at interview difficulty
Complete guide to every SQL topic tested in data engineering interviews