CASE Expression Syntax
There are two forms of the CASE expression. Both evaluate conditions top-to-bottom and return the result for the first matching branch. The ELSE clause is optional β if omitted and no branch matches, CASE returns NULL.
-- Simple CASE: compare one expression to multiple values
CASE column_or_expression
WHEN value1 THEN result1
WHEN value2 THEN result2
ELSE default_result
END
-- Searched CASE: evaluate arbitrary boolean conditions
CASE
WHEN condition1 THEN result1
WHEN condition2 THEN result2
ELSE default_result
END
Simple CASE
Simple CASE compares a single expression to a list of values β like a switch/match statement in programming languages.
-- Map numeric status codes to readable labels
SELECT
order_id,
CASE status
WHEN 1 THEN 'Pending'
WHEN 2 THEN 'Processing'
WHEN 3 THEN 'Shipped'
WHEN 4 THEN 'Delivered'
WHEN 5 THEN 'Cancelled'
ELSE 'Unknown'
END AS status_label
FROM orders;
-- Map country codes to regions
SELECT
customer_id,
country_code,
CASE country_code
WHEN 'US' THEN 'North America'
WHEN 'CA' THEN 'North America'
WHEN 'GB' THEN 'Europe'
WHEN 'DE' THEN 'Europe'
WHEN 'JP' THEN 'Asia Pacific'
ELSE 'Other'
END AS region
FROM customers;
Searched CASE
Searched CASE evaluates a full boolean condition for each branch. More powerful than Simple CASE β use it when you need ranges, multiple columns, or complex logic.
-- Assign salary bands
SELECT
employee_id,
name,
salary,
CASE
WHEN salary < 30000 THEN 'Entry Level'
WHEN salary < 60000 THEN 'Mid Level'
WHEN salary < 100000 THEN 'Senior'
ELSE 'Executive'
END AS salary_band
FROM employees;
-- Classify orders by size
SELECT
order_id,
total_amount,
CASE
WHEN total_amount < 50 THEN 'Small'
WHEN total_amount < 200 THEN 'Medium'
WHEN total_amount < 1000 THEN 'Large'
ELSE 'Enterprise'
END AS order_size
FROM orders;
CASE in SELECT
The most common use of CASE: create a computed column based on row values. The result can be used in WHERE, ORDER BY, or GROUP BY by referencing the alias or repeating the expression.
-- Add discount tiers inline
SELECT
product_name,
price,
CASE
WHEN price > 500 THEN ROUND(price * 0.80, 2)
WHEN price > 100 THEN ROUND(price * 0.90, 2)
ELSE price
END AS discounted_price,
CASE
WHEN price > 500 THEN '20% off'
WHEN price > 100 THEN '10% off'
ELSE 'No discount'
END AS discount_label
FROM products;
-- Flag late orders
SELECT
order_id,
order_date,
ship_date,
CASE
WHEN ship_date IS NULL THEN 'Not Shipped'
WHEN ship_date > order_date + INTERVAL 3 DAY THEN 'Late'
ELSE 'On Time'
END AS shipping_status
FROM orders;
CASE in ORDER BY
CASE inside ORDER BY enables custom sort sequences that are impossible with plain column ordering.
-- Custom priority order: Critical β High β Medium β Low
SELECT ticket_id, title, priority
FROM support_tickets
ORDER BY
CASE priority
WHEN 'Critical' THEN 1
WHEN 'High' THEN 2
WHEN 'Medium' THEN 3
WHEN 'Low' THEN 4
ELSE 5
END;
-- Put active records first, then sort by name
SELECT customer_id, name, status
FROM customers
ORDER BY
CASE WHEN status = 'active' THEN 0 ELSE 1 END,
name;
CASE in Aggregate Functions (Pivoting)
Combining CASE with SUM or COUNT is the standard SQL technique for pivoting rows into columns β creating a cross-tabulation without a PIVOT keyword.
-- Count orders by status in separate columns (pivot)
SELECT
COUNT(*) AS total_orders,
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending,
SUM(CASE WHEN status = 'shipped' THEN 1 ELSE 0 END) AS shipped,
SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) AS delivered,
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) AS cancelled
FROM orders;
-- Monthly revenue by category in one row per month
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS month,
SUM(CASE WHEN category = 'Electronics' THEN amount ELSE 0 END) AS electronics,
SUM(CASE WHEN category = 'Clothing' THEN amount ELSE 0 END) AS clothing,
SUM(CASE WHEN category = 'Books' THEN amount ELSE 0 END) AS books
FROM order_items
JOIN products USING (product_id)
GROUP BY month
ORDER BY month;
-- Alternative: COUNT(CASE WHEN ...) returns 0 for no matches
-- because COUNT only counts non-NULL, and the WHEN returns NULL when false
SELECT
COUNT(CASE WHEN score >= 90 THEN 1 END) AS grade_a,
COUNT(CASE WHEN score >= 80 AND score < 90 THEN 1 END) AS grade_b,
COUNT(CASE WHEN score < 80 THEN 1 END) AS grade_c
FROM exam_results;
Summary
π Summary
- Simple CASE β Compares one value to a list. Like a switch statement.
- Searched CASE β Evaluates conditions in order. Returns first TRUE match.
- ELSE is optional β Without ELSE, unmatched rows return NULL.
- CASE in SELECT β Compute labels, tiers, flags, or derived columns.
- CASE in ORDER BY β Create custom sort sequences for priorities or statuses.
- CASE in SUM/COUNT β Pivot rows to columns. The core technique for cross-tabulation.
- IIF() (SQL Server) β Shorthand:
IIF(condition, true_val, false_val). - IF() (MySQL) β Shorthand:
IF(condition, true_val, false_val).
FAQ
Yes, SQL CASE is guaranteed by the standard to evaluate conditions left-to-right and stop at the first TRUE condition. This matters when later conditions would cause errors β for example, CASE WHEN col IS NOT NULL WHEN col > 0 THEN 'positive' END safely avoids comparing NULL to 0.
Yes, CASE can be nested: CASE WHEN x > 10 THEN CASE WHEN y > 5 THEN 'High-High' ELSE 'High-Low' END ELSE 'Low' END. However, deeply nested CASE expressions become hard to read. Consider a flat searched CASE with compound conditions instead: CASE WHEN x > 10 AND y > 5 THEN 'High-High' ....
Simple CASE (CASE col WHEN val THEN ...) uses equality comparison only. Searched CASE (CASE WHEN condition THEN ...) evaluates any boolean expression β ranges, multiple columns, function calls, IS NULL. Simple CASE is more readable for exact value matching; Searched CASE is required for anything more complex.
Yes: WHERE CASE WHEN col IS NULL THEN 'unknown' ELSE col END = 'active'. However, this often prevents index usage. A more efficient alternative is usually: WHERE (col = 'active' OR col IS NULL). Use CASE in WHERE when the logic genuinely cannot be expressed with standard boolean operators.