SQL CASE WHEN: Syntax, Multiple Conditions and Examples
CASE WHEN adds if/else logic to a SQL query: it checks conditions in order and returns the value of the first one that is true. You use it to label rows ("High", "Medium", "Low"), to calculate different values for different rows, and inside SUM or COUNT to total only the rows that match.
What Is CASE WHEN in SQL?
A CASE expression works like a teacher grading papers: 90 or more is an A, 75 or more a B, 60 or more a C, and anything else a fail. The teacher checks the rules from the top and stops at the first one that fits. CASE WHEN does exactly that for every row:
WITH students(name, marks) AS (
VALUES ('Ishaan', 94), ('Divya', 81), ('Karan', 67), ('Sneha', 45), ('Arjun', 60)
)
SELECT name, marks,
CASE
WHEN marks >= 90 THEN 'A'
WHEN marks >= 75 THEN 'B'
WHEN marks >= 60 THEN 'C'
ELSE 'Fail'
END AS grade
FROM students;
▶ Run this query in the SQL Playground
| name | marks | grade |
|---|---|---|
| Ishaan | 94 | A |
| Divya | 81 | B |
| Karan | 67 | C |
| Sneha | 45 | Fail |
| Arjun | 60 | C |
Ishaan's 94 matches the first condition, so the later ones are never checked. Arjun's 60 fails the first two and matches marks >= 60. Sneha's 45 matches nothing, so she gets the ELSE value. The five students are written into the query with WITH ... VALUES, so you can change their marks in the Playground and see the grades move.
| Business question | CASE WHEN answer |
|---|---|
| Label employees by salary band | WHEN salary >= 150000 THEN 'High' |
| Turn status codes into readable text | CASE status WHEN 'delivered' THEN 'Complete' ... |
| Count orders per status in one row | SUM(CASE WHEN status = 'shipped' THEN 1 ELSE 0 END) |
| Sort by business priority, not alphabetically | ORDER BY CASE status WHEN 'pending' THEN 1 ... END |
| Flag missing data | WHEN shipped_date IS NULL THEN 'Not shipped' |
CASE WHEN Syntax
CASE
WHEN condition_1 THEN result_1
WHEN condition_2 THEN result_2
...
ELSE default_result
END
| Part | What it does |
|---|---|
CASE | Starts the expression |
WHEN condition | A test that is true or false for the row, such as salary >= 150000 |
THEN result | The value returned when that test is the first true one |
ELSE default | Optional. The value when no condition is true; without it the result is NULL |
END | Closes the expression. Forgetting it is the most common syntax error |
There are no commas between the WHEN branches. The whole expression is one value, so give it a name with AS, exactly as you would any calculated column. CASE is part of standard SQL and is written the same way in MySQL, PostgreSQL, SQL Server, Oracle and SQLite.
Multiple Conditions and Condition Order
Most real CASE expressions have several WHEN branches. Here every employee gets a salary band:
SELECT first_name, last_name, salary,
CASE
WHEN salary >= 150000 THEN 'High'
WHEN salary >= 90000 THEN 'Medium'
ELSE 'Low'
END AS salary_band
FROM employees
ORDER BY id
LIMIT 6;
▶ Run this query in the SQL Playground
| first_name | last_name | salary | salary_band |
|---|---|---|---|
| William | Lewis | 61774 | Low |
| John | Miller | 191694 | High |
| Daniel | Rivera | 135067 | Medium |
| Melissa | Smith | 51465 | Low |
| Jason | Mitchell | 156865 | High |
| Amy | Nguyen | 194543 | High |
Order matters. The conditions overlap: a salary of 160,000 is both >= 150000 and >= 90000. Because CASE stops at the first true condition, the highest threshold has to come first. Swap the two branches and every high earner is labelled "Medium", as the mistakes section shows.
Combining tests with AND and OR
One WHEN can test several columns at once. This marks employees who are both in Engineering and hired before 2018:
SELECT first_name, department, hire_date,
CASE
WHEN department = 'Engineering' AND hire_date < '2018-01-01' THEN 'Senior engineer'
WHEN department = 'Engineering' THEN 'Engineer'
WHEN department IN ('Sales', 'Marketing') THEN 'Commercial'
ELSE 'Other'
END AS employee_group
FROM employees
WHERE department IN ('Engineering', 'Sales', 'Marketing')
ORDER BY department, hire_date
LIMIT 8;
▶ Run this query in the SQL Playground
| first_name | department | hire_date | employee_group |
|---|---|---|---|
| Daniel | Engineering | 2016-06-06 | Senior engineer |
| Donna | Engineering | 2018-06-21 | Engineer |
| Chris | Engineering | 2019-03-07 | Engineer |
| Anthony | Engineering | 2019-04-04 | Engineer |
| Barbara | Engineering | 2019-12-21 | Engineer |
| Susan | Engineering | 2020-05-19 | Engineer |
| Dorothy | Engineering | 2021-04-21 | Engineer |
| Melissa | Engineering | 2022-04-13 | Engineer |
Simple CASE vs Searched CASE
Everything so far has been a searched CASE: each WHEN has its own condition. When you only compare one column with fixed values, the simple CASE form is shorter. The column comes once, after CASE, and each WHEN lists a value:
SELECT status,
CASE status
WHEN 'pending' THEN 'Waiting'
WHEN 'processing' THEN 'Waiting'
WHEN 'shipped' THEN 'On the way'
WHEN 'delivered' THEN 'Complete'
ELSE 'Closed'
END AS customer_label,
COUNT(*) AS orders
FROM orders
GROUP BY status
ORDER BY orders DESC;
▶ Run this query in the SQL Playground
| status | customer_label | orders |
|---|---|---|
| delivered | Complete | 82 |
| processing | Waiting | 28 |
| cancelled | Closed | 27 |
| pending | Waiting | 23 |
| shipped | On the way | 21 |
| refunded | Closed | 19 |
| Simple CASE | Searched CASE | |
|---|---|---|
| Form | CASE status WHEN 'shipped' THEN ... | CASE WHEN status = 'shipped' THEN ... |
| Can test | Equality with one expression | Any condition: ranges, LIKE, IS NULL, several columns |
| Use it for | Translating codes into labels | Everything else |
CASE col WHEN NULL THEN ... compares col = NULL, which is never true. Use the searched form with WHEN col IS NULL.Calculations in THEN and ELSE
THEN and ELSE can return the result of a calculation, not just a label. That covers bonuses, tax slabs, commission rates and shipping fees: a different formula depending on a threshold. This gives a bonus that grows with length of service:
SELECT first_name, hire_date, salary,
CASE
WHEN hire_date < '2018-01-01' THEN ROUND(salary * 0.10)
WHEN hire_date < '2022-01-01' THEN ROUND(salary * 0.05)
ELSE ROUND(salary * 0.02)
END AS bonus
FROM employees
ORDER BY id
LIMIT 6;
▶ Run this query in the SQL Playground
| first_name | hire_date | salary | bonus |
|---|---|---|---|
| William | 2022-07-24 | 61774 | 1235 |
| John | 2021-08-09 | 191694 | 9585 |
| Daniel | 2016-06-06 | 135067 | 13507 |
| Melissa | 2022-04-13 | 51465 | 1029 |
| Jason | 2019-10-17 | 156865 | 7843 |
| Amy | 2021-01-16 | 194543 | 9727 |
The same pattern gives tiered shipping fees on orders:
SELECT id, total,
CASE
WHEN total >= 1000 THEN 0
WHEN total >= 250 THEN 9.99
ELSE 14.99
END AS shipping_fee
FROM orders
ORDER BY id
LIMIT 6;
▶ Run this query in the SQL Playground
| id | total | shipping_fee |
|---|---|---|
| 1 | 2638.11 | 0 |
| 2 | 2947 | 0 |
| 3 | 683.24 | 9.99 |
| 4 | 3920.44 | 0 |
| 5 | 1120.99 | 0 |
| 6 | 1327.01 | 0 |
Keep every branch the same type: all numbers here. Mixing text in one branch and a number in another is an error in PostgreSQL and SQL Server and gives surprising results elsewhere.
CASE in WHERE and ORDER BY
CASE can appear anywhere a value is allowed. In ORDER BY it gives a custom sort order. Plain ASC or DESC would sort statuses alphabetically; this sorts them in the order an order moves through:
SELECT id, status, order_date
FROM orders
ORDER BY CASE status
WHEN 'pending' THEN 1
WHEN 'processing' THEN 2
WHEN 'shipped' THEN 3
WHEN 'delivered' THEN 4
ELSE 5
END,
order_date DESC
LIMIT 8;
▶ Run this query in the SQL Playground
| id | status | order_date |
|---|---|---|
| 70 | pending | 2024-12-24 |
| 168 | pending | 2024-11-01 |
| 112 | pending | 2024-07-06 |
| 76 | pending | 2024-03-14 |
| 147 | pending | 2024-03-08 |
| 129 | pending | 2024-02-17 |
| 193 | pending | 2023-12-11 |
| 72 | pending | 2023-11-08 |
In WHERE, CASE lets the threshold depend on the row. Here Sales employees qualify above 150,000 and everyone else above 190,000:
SELECT first_name, department, salary
FROM employees
WHERE salary > CASE WHEN department = 'Sales' THEN 150000 ELSE 190000 END
ORDER BY salary DESC;
▶ Run this query in the SQL Playground
| first_name | department | salary |
|---|---|---|
| Steven | Marketing | 204449 |
| Nancy | Customer Success | 203999 |
| John | Operations | 203467 |
| Donna | Engineering | 199029 |
| Matthew | Marketing | 196530 |
| Paul | Finance | 196032 |
| Laura | Sales | 195759 |
| Dorothy | Engineering | 195690 |
| Linda | Finance | 194604 |
| Amy | Design | 194543 |
This works, but the same filter written as plain conditions is easier to read and lets the database use an index on salary or department: WHERE (department = 'Sales' AND salary > 150000) OR salary > 190000. Reach for CASE in WHERE only when the plain form becomes hard to follow.
GROUP BY a CASE Expression
Grouping by a CASE expression creates your own categories on the fly, without a lookup table. Count the employees in each salary band and show the average salary:
SELECT CASE
WHEN salary >= 150000 THEN 'High'
WHEN salary >= 90000 THEN 'Medium'
ELSE 'Low'
END AS salary_band,
COUNT(*) AS employees,
ROUND(AVG(salary)) AS avg_salary
FROM employees
GROUP BY salary_band
ORDER BY avg_salary DESC;
▶ Run this query in the SQL Playground
| salary_band | employees | avg_salary |
|---|---|---|
| High | 40 | 180517 |
| Medium | 43 | 118566 |
| Low | 37 | 70419 |
The 40 employees in the High band average 180517. SQLite, MySQL and PostgreSQL allow GROUP BY salary_band, the alias from SELECT. SQL Server and Oracle do not, so there you repeat the whole CASE expression in GROUP BY.
Conditional Aggregation: CASE Inside SUM and COUNT
This is the most useful CASE pattern in reporting. Put a CASE inside an aggregate and the aggregate only "sees" the rows you choose. One pass over the table can then produce several totals side by side. Here is every year's order count, broken down by outcome:
SELECT strftime('%Y', order_date) AS year,
COUNT(*) AS all_orders,
SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) AS delivered,
SUM(CASE WHEN status IN ('cancelled', 'refunded') THEN 1 ELSE 0 END) AS cancelled_or_refunded,
ROUND(SUM(CASE WHEN status = 'delivered' THEN total ELSE 0 END), 2) AS delivered_revenue
FROM orders
GROUP BY year
ORDER BY year;
▶ Run this query in the SQL Playground
| year | all_orders | delivered | cancelled_or_refunded | delivered_revenue |
|---|---|---|---|---|
| 2022 | 62 | 24 | 13 | 65652.19 |
| 2023 | 72 | 30 | 21 | 70494.51 |
| 2024 | 66 | 28 | 12 | 74629.64 |
SUM(CASE WHEN ... THEN 1 ELSE 0 END) counts the matching rows; SUM(CASE WHEN ... THEN total ELSE 0 END) adds up their totals. Without conditional aggregation this would take one query per column, or a GROUP BY that returns one row per year and status instead of one row per year.
COUNT with a CASE and no ELSE
COUNT ignores NULLs, and a CASE without ELSE returns NULL when nothing matches. So COUNT(CASE WHEN ... THEN 1 END) counts only the matching rows:
SELECT department,
COUNT(*) AS employees,
COUNT(CASE WHEN salary >= 150000 THEN 1 END) AS high_earners,
COUNT(CASE WHEN status = 'on_leave' THEN 1 END) AS on_leave
FROM employees
GROUP BY department
ORDER BY high_earners DESC, department;
▶ Run this query in the SQL Playground
| department | employees | high_earners | on_leave |
|---|---|---|---|
| Customer Success | 15 | 7 | 2 |
| Operations | 13 | 6 | 3 |
| Design | 10 | 5 | 1 |
| Engineering | 11 | 4 | 0 |
| Marketing | 13 | 4 | 0 |
| Finance | 10 | 3 | 0 |
| HR | 12 | 3 | 3 |
| Legal | 16 | 3 | 4 |
| Sales | 11 | 3 | 0 |
| Product | 9 | 2 | 1 |
Conditional aggregates also work in HAVING. To keep only the departments with at least five high earners, add HAVING COUNT(CASE WHEN salary >= 150000 THEN 1 END) >= 5.
CASE WHEN With Dates
Dates are a common thing to classify: weekday or weekend, quarter, how recent. In SQLite, strftime('%w', date) returns the day of the week as text from 0 (Sunday) to 6 (Saturday):
SELECT CASE WHEN strftime('%w', order_date) IN ('0', '6') THEN 'Weekend'
ELSE 'Weekday'
END AS day_type,
COUNT(*) AS orders,
ROUND(AVG(total), 2) AS avg_order_value
FROM orders
GROUP BY day_type;
▶ Run this query in the SQL Playground
| day_type | orders | avg_order_value |
|---|---|---|
| Weekday | 137 | 2436.85 |
| Weekend | 63 | 2811.46 |
A company whose financial year starts in April needs its own quarter labels. CASE maps calendar months to fiscal quarters:
SELECT CASE
WHEN CAST(strftime('%m', order_date) AS INTEGER) BETWEEN 4 AND 6 THEN 'Q1 (Apr-Jun)'
WHEN CAST(strftime('%m', order_date) AS INTEGER) BETWEEN 7 AND 9 THEN 'Q2 (Jul-Sep)'
WHEN CAST(strftime('%m', order_date) AS INTEGER) BETWEEN 10 AND 12 THEN 'Q3 (Oct-Dec)'
ELSE 'Q4 (Jan-Mar)'
END AS fiscal_quarter,
COUNT(*) AS orders
FROM orders
GROUP BY fiscal_quarter
ORDER BY fiscal_quarter;
▶ Run this query in the SQL Playground
| fiscal_quarter | orders |
|---|---|
| Q1 (Apr-Jun) | 46 |
| Q2 (Jul-Sep) | 45 |
| Q3 (Oct-Dec) | 59 |
| Q4 (Jan-Mar) | 50 |
Other databases extract the parts differently (EXTRACT(DOW FROM order_date) in PostgreSQL, DAYOFWEEK() in MySQL, DATEPART(weekday, ...) in SQL Server), but the CASE around them is the same. See SQL date functions.
CASE WHEN and NULL Values
To test for a missing value inside CASE, use IS NULL. An order with no shipped_date has not been shipped:
SELECT CASE WHEN shipped_date IS NULL THEN 'Not shipped'
ELSE 'Shipped'
END AS shipping_state,
COUNT(*) AS orders
FROM orders
GROUP BY shipping_state;
▶ Run this query in the SQL Playground
| shipping_state | orders |
|---|---|
| Not shipped | 97 |
| Shipped | 103 |
Two rules to remember. First, WHEN shipped_date = NULL is never true, so every row would fall through to ELSE. Second, a CASE with no ELSE returns NULL for rows that match nothing; that is often how blank categories appear in a report. When you only need "this value, or a default if it is NULL", COALESCE is shorter than a CASE.
CASE WHEN With Text Patterns
Conditions can use LIKE and string functions. This segments users by email provider:
SELECT CASE
WHEN email LIKE '%@gmail.com' THEN 'Gmail'
WHEN email LIKE '%@yahoo.com' THEN 'Yahoo'
WHEN email LIKE '%@hotmail.com' OR email LIKE '%@outlook.com' THEN 'Microsoft'
ELSE 'Other'
END AS provider,
COUNT(*) AS users
FROM users
GROUP BY provider
ORDER BY users DESC;
▶ Run this query in the SQL Playground
| provider | users |
|---|---|
| Microsoft | 48 |
| Other | 45 |
| Yahoo | 29 |
| Gmail | 28 |
CASE WHEN With Subqueries
A condition can compare with the result of a subquery. This labels each employee against the company-wide average salary:
SELECT first_name, department, salary,
CASE WHEN salary > (SELECT AVG(salary) FROM employees) THEN 'Above average'
ELSE 'At or below average'
END AS compared_with_company
FROM employees
ORDER BY id
LIMIT 6;
▶ Run this query in the SQL Playground
| first_name | department | salary | compared_with_company |
|---|---|---|---|
| William | Customer Success | 61774 | At or below average |
| John | Legal | 191694 | Above average |
| Daniel | Engineering | 135067 | Above average |
| Melissa | Engineering | 51465 | At or below average |
| Jason | Design | 156865 | Above average |
| Amy | Design | 194543 | Above average |
Make the subquery correlated and the comparison is with each employee's own department instead: (SELECT AVG(salary) FROM employees e2 WHERE e2.department = e.department). The subquery must return a single value; one that can return several rows makes the query fail.
Nested CASE Expressions
A THEN or ELSE can contain another CASE. That is legal, but it gets hard to read quickly. Both queries below label products the same way; the second is flat:
-- Nested
CASE WHEN status = 'discontinued' THEN 'Discontinued'
ELSE CASE WHEN stock < 50 THEN 'Reorder now'
ELSE 'In stock'
END
END
SELECT name, status, stock,
CASE
WHEN status = 'discontinued' THEN 'Discontinued'
WHEN stock < 50 THEN 'Reorder now'
ELSE 'In stock'
END AS stock_label
FROM products
WHERE status = 'discontinued' OR stock < 50
ORDER BY stock_label, stock;
▶ Run this query in the SQL Playground
| name | status | stock | stock_label |
|---|---|---|---|
| The Manager's Path | discontinued | 62 | Discontinued |
| Succulent Plant Set | discontinued | 107 | Discontinued |
| Retinol Eye Cream 30ml | discontinued | 227 | Discontinued |
| Sticky Notes Assorted 18pk | discontinued | 285 | Discontinued |
| Artisan Hot Sauce Trio | discontinued | 391 | Discontinued |
| 4-Person Camping Tent | discontinued | 394 | Discontinued |
| Smart Thermostat | discontinued | 401 | Discontinued |
| Leather Steering Wheel Cover | active | 10 | Reorder now |
| Giant Stuffed Teddy Bear | active | 12 | Reorder now |
| Winter Parka Jacket | active | 33 | Reorder now |
Because CASE stops at the first true condition, a flat list with the most specific rule first does the same job as nesting. Keep nesting for cases where the inner decision really depends on the outer one, and no deeper than two levels.
CASE WHEN in MySQL, PostgreSQL, SQL Server and SQLite
| Feature | MySQL | PostgreSQL | SQL Server | SQLite |
|---|---|---|---|---|
| Searched and simple CASE | Yes | Yes | Yes | Yes |
| Two-way shortcut | IF(cond, a, b) | None | IIF(cond, a, b) | IIF(cond, a, b) (3.32+) |
| FILTER on aggregates | No | Yes | No | Yes (3.30+) |
| GROUP BY a SELECT alias | Yes | Yes | No | Yes |
IIF and FILTER are shorter ways to write the same thing. In the Playground both work:
SELECT COUNT(*) FILTER (WHERE status = 'delivered') AS delivered_filter,
SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) AS delivered_case,
SUM(IIF(status = 'delivered', 1, 0)) AS delivered_iif
FROM orders;
▶ Run this query in the SQL Playground
| delivered_filter | delivered_case | delivered_iif |
|---|---|---|
| 82 | 82 | 82 |
All three return 82. CASE is the one that runs everywhere, so it is the safe choice for queries that may move between databases.
Common CASE WHEN Mistakes
Six errors, most of which give wrong answers without an error message.
1. Missing END
SELECT first_name,
CASE WHEN salary >= 150000 THEN 'High'
FROM employees;
SELECT first_name,
CASE WHEN salary >= 150000 THEN 'High' ELSE 'Other' END AS band
FROM employees;
Every CASE must end with END. The database usually reports the error near FROM, the first word it did not expect.
2. Conditions in the wrong order
CASE
WHEN salary >= 90000 THEN 'Medium'
WHEN salary >= 150000 THEN 'High'
ELSE 'Low'
END
CASE
WHEN salary >= 150000 THEN 'High'
WHEN salary >= 90000 THEN 'Medium'
ELSE 'Low'
END
Every salary of 150,000 or more is also 90,000 or more, so the first branch catches it and the High branch can never be reached. With overlapping ranges, put the most restrictive condition first.
3. Forgetting ELSE
CASE WHEN department = 'Sales' THEN 'Sales team' END
CASE WHEN department = 'Sales' THEN 'Sales team'
ELSE 'Other team' END
Without ELSE, every row that matches no WHEN gets NULL. That is valid SQL, but usually shows up later as an unexplained blank category.
4. Testing NULL with =
CASE WHEN shipped_date = NULL THEN 'Not shipped'
ELSE 'Shipped' END
CASE WHEN shipped_date IS NULL THEN 'Not shipped'
ELSE 'Shipped' END
A comparison with NULL is never true, so the first branch never matches. See IS NULL.
5. Mixing data types between branches
CASE WHEN salary >= 150000 THEN 'High' ELSE 0 END
CASE WHEN salary >= 150000 THEN 'High' ELSE 'Other' END
PostgreSQL and SQL Server reject the first version or try to convert "High" to a number. Keep every THEN and ELSE the same type.
6. Wrong boundary: > instead of >=
CASE WHEN salary > 90000 THEN 'Medium' ELSE 'Low' END
CASE WHEN salary >= 90000 THEN 'Medium' ELSE 'Low' END
The query runs either way; only one matches the business rule. Check the boundary values themselves (exactly 90,000) when you test a CASE expression.
CASE WHEN and Performance
CASE in SELECT costs very little: it is evaluated once per row that is already being returned. Two places deserve more care:
| Where | Effect | What to do |
|---|---|---|
| WHERE | A column wrapped in CASE usually cannot use an index, just like a column wrapped in a function | Rewrite as plain AND / OR conditions when you can |
| GROUP BY | The CASE runs for every row before grouping | Fine for most tables; on very large ones store the category in a column |
| Many conditional aggregates | One pass over the table, however many columns | Usually faster than several separate queries |
Use EXPLAIN to check whether a query uses an index before rewriting it.
CASE WHEN Cheat Sheet
| Pattern | Example |
|---|---|
| Label by range | CASE WHEN salary >= 150000 THEN 'High' WHEN salary >= 90000 THEN 'Medium' ELSE 'Low' END |
| Translate codes | CASE status WHEN 'shipped' THEN 'On the way' ELSE 'Other' END |
| Count matching rows | SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) |
| Total matching rows | SUM(CASE WHEN status = 'delivered' THEN total ELSE 0 END) |
| Count without ELSE | COUNT(CASE WHEN salary >= 150000 THEN 1 END) |
| Custom sort | ORDER BY CASE status WHEN 'pending' THEN 1 WHEN 'shipped' THEN 2 ELSE 3 END |
| Missing values | CASE WHEN shipped_date IS NULL THEN 'Not shipped' ELSE 'Shipped' END |
| Own categories | GROUP BY CASE WHEN ... END |
Practice CASE WHEN Queries
6 exercises on the real Playground tables.
Write each query yourself first. Every solution has been run against the Playground database.
-
1Easy Label every product as "Budget" (under 100), "Mid-range" (100 to under 1,000) or "Premium" (1,000 and up), and count the products in each label. Table:
productsShow solution
SELECT CASE WHEN price < 100 THEN 'Budget' WHEN price < 1000 THEN 'Mid-range' ELSE 'Premium' END AS price_band, COUNT(*) AS products FROM products GROUP BY price_band ORDER BY products DESC;▶ Run this query in the SQL Playground
Going from the lowest threshold upward with < works as well as going down with >=; what matters is that the order matches the comparisons.
-
2Easy Classify reviews as "Positive" (4-5 stars), "Neutral" (3) or "Negative" (1-2) and count each group. Table:
reviewsShow solution
SELECT CASE WHEN rating >= 4 THEN 'Positive' WHEN rating = 3 THEN 'Neutral' ELSE 'Negative' END AS sentiment, COUNT(*) AS reviews FROM reviews GROUP BY sentiment ORDER BY reviews DESC;▶ Run this query in the SQL Playground
The rating column is 1 to 5, so ELSE covers exactly 1 and 2.
-
3Medium For each payment method, show the number of payments and how many of them succeeded and failed, in one row per method. Table:
paymentsShow solution
SELECT method, COUNT(*) AS payments, SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) AS succeeded, SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed FROM payments GROUP BY method ORDER BY payments DESC;▶ Run this query in the SQL Playground
Conditional aggregation turns the status values into columns, so each method needs only one row.
-
4Medium Count premium and regular users per country, side by side. Table:
usersShow solution
SELECT country, SUM(CASE WHEN is_premium = 1 THEN 1 ELSE 0 END) AS premium, SUM(CASE WHEN is_premium = 0 THEN 1 ELSE 0 END) AS regular FROM users GROUP BY country ORDER BY premium + regular DESC;▶ Run this query in the SQL Playground
Each SUM counts only the rows its CASE lets through; ORDER BY can sort by the sum of the two.
-
5Medium List the 10 most recent orders with a column "size" that is "Large" for totals of 2,000 or more and "Standard" otherwise. Table:
ordersShow solution
SELECT id, order_date, total, CASE WHEN total >= 2000 THEN 'Large' ELSE 'Standard' END AS size FROM orders ORDER BY order_date DESC, id DESC LIMIT 10;▶ Run this query in the SQL Playground
A two-branch CASE is the portable version of IF() or IIF().
-
6Hard For each department, show the share of employees (in percent, one decimal) who earn 150,000 or more. Sort from the highest share. Table:
employeesShow solution
SELECT department, COUNT(*) AS employees, ROUND(100.0 * SUM(CASE WHEN salary >= 150000 THEN 1 ELSE 0 END) / COUNT(*), 1) AS pct_high_earners FROM employees GROUP BY department ORDER BY pct_high_earners DESC, department;▶ Run this query in the SQL Playground
Multiplying by 100.0 first makes the division decimal; integer division would round every share down to a whole number.
📝 Quiz — CASE WHEN
Test your understanding · 0/29 answered
What is the main purpose of CASE WHEN in SQL?
CASE WHEN checks conditions in order and returns a value depending on which one is true.
Which keyword ends every CASE expression?
Every CASE expression must end with END.
What happens if no WHEN condition matches and there is no ELSE?
Without ELSE, rows that match nothing get NULL.
In what order are the WHEN conditions checked?
CASE checks the conditions from the top and stops at the first true one, so order matters for overlapping ranges.
For salary = 160000, what does CASE WHEN salary >= 50000 THEN 'Medium' WHEN salary >= 100000 THEN 'High' ELSE 'Low' END return?
160000 is >= 50000, so the first branch matches and 'High' is never checked. The thresholds are in the wrong order.
Is CASE WHEN similar to IF / ELSE in programming?
It is the same idea, but CASE is an expression that returns a value inside a query.
Can a CASE expression be used inside SUM()?
SUM, COUNT, AVG, MIN and MAX all accept a CASE expression, which is how one query produces several category totals.
What does COUNT(CASE WHEN department = 'Sales' THEN 1 END) count?
Non-matching rows return NULL, and COUNT skips NULLs.
Can you GROUP BY a CASE expression?
Grouping by a CASE expression groups rows by its result, such as High / Medium / Low.
How do you test for NULL inside CASE?
A comparison with NULL is never true; only IS NULL works.
What is conditional aggregation?
For example SUM(CASE WHEN status = 'delivered' THEN total ELSE 0 END) adds up only the delivered orders.
Can one CASE be nested inside another?
A THEN or ELSE can hold another CASE. A flat CASE with combined conditions is usually clearer.
Where can a CASE expression appear?
CASE is an expression, so it can go anywhere a value is allowed.
What is CASE in ORDER BY typically used for?
ORDER BY CASE status WHEN 'pending' THEN 1 ... END sorts by business priority.
What is the rule for the data types of THEN and ELSE?
Mixing types causes errors in some databases and odd conversions in others.
How does a simple CASE (CASE col WHEN value ...) differ from a searched CASE?
The simple form tests equality with one expression. Ranges, LIKE and IS NULL need the searched form.
Can CASE WHEN be used in a WHERE clause?
It works, but plain AND / OR conditions are usually easier to read and can use indexes.
What is CASE WHEN combined with LIKE commonly used for?
For example WHEN email LIKE '%@gmail.com' THEN 'Gmail'.
Why is CASE WHEN so common in analytics work?
Categories such as "High value customer" or "At risk" are usually defined with a CASE expression.
Is CASE WHEN a common SQL interview topic?
Turning rows into category columns with SUM(CASE ...) is one of the most often tested practical skills.
What does SELECT name, CASE WHEN rating >= 4 THEN 'Positive' WHEN rating = 3 THEN 'Neutral' ELSE 'Negative' END FROM reviews classify?
It turns a 1-5 star rating into three readable groups.
What is the best practice for nested CASE expressions?
Flat CASE expressions are easier to read and to test.
What does CASE WHEN phone IS NULL THEN 'Missing' ELSE 'Available' END detect?
It is the standard pattern for flagging missing data.
What does "wrong business logic" in a CASE expression usually mean?
These bugs give no error. Test the exact boundary values to find them.
What is a performance risk of CASE inside WHERE?
Like a function around a column, it hides the column from a plain index.
How does CASE WHEN syntax differ between MySQL, SQL Server and PostgreSQL?
CASE is part of the SQL standard. Shortcuts such as IF() and IIF() differ, CASE does not.
What can EXPLAIN tell you about a query with CASE in WHERE?
EXPLAIN shows the query plan, including index use.
How should you test a CASE expression?
Wrong order and wrong boundaries are silent logic bugs, so check the edge values on purpose.
What separates CASE WHEN from a stored procedure's IF statement?
CASE produces a value per row; IF is control flow in procedural code.
CASE WHEN Interview Questions
Q1. What is the difference between a simple and a searched CASE?
A simple CASE compares one expression with a list of values: CASE status WHEN 'shipped' THEN .... A searched CASE evaluates a separate condition in each WHEN, so it can test ranges, several columns, LIKE patterns and IS NULL.
Q2. What does a CASE expression return when nothing matches?
The ELSE value, or NULL if there is no ELSE.
Q3. How do you pivot rows into columns with CASE?
Wrap a CASE in an aggregate for each column you want: SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) AS delivered, one per status, grouped by whatever should stay as rows.
Q4. Why does the order of WHEN branches matter?
CASE stops at the first true condition. With overlapping ranges, a broad condition placed first catches rows meant for a narrower one, so the most restrictive condition has to come first.
Q5. What is the difference between WHERE and CASE WHEN?
WHERE removes rows that do not match. CASE WHEN keeps every row and gives it a different value depending on the condition. They are often used together.
Q6. How would you count distinct customers who placed a delivered order, per year?
COUNT(DISTINCT CASE WHEN status = 'delivered' THEN user_id END) grouped by year. Non-matching rows give NULL, which COUNT DISTINCT ignores.
Q7. Can you use a column alias from a CASE expression in GROUP BY?
In SQLite, MySQL and PostgreSQL, yes. SQL Server and Oracle do not allow it, so you repeat the expression or wrap the query in a subquery or CTE.
Q8. When would you use COALESCE instead of CASE?
When the only question is "is this NULL?". COALESCE(phone, 'n/a') is the short form of CASE WHEN phone IS NULL THEN 'n/a' ELSE phone END.
Q9. How do you defend a CASE classification in a code review?
Show test rows for each boundary value, show that the branch order handles overlapping ranges, confirm what ELSE catches, and check that the thresholds match the business definition.
Frequently Asked Questions
What is CASE WHEN in SQL?
CASE WHEN is an expression that adds if/else logic to a query. It checks a list of conditions in order and returns the value of the first one that is true, or the ELSE value if none is.
Is CASE WHEN similar to IF ELSE?
Yes. Each WHEN works like IF or ELSE IF and ELSE is the final fallback. The difference is that CASE returns a value inside a query rather than choosing which code to run.
Can CASE WHEN have multiple conditions?
Yes. A CASE expression can have any number of WHEN branches, and each WHEN can combine tests with AND and OR. They are checked from the top and the first true one wins.
What is conditional aggregation?
Conditional aggregation puts a CASE expression inside an aggregate, for example SUM(CASE WHEN status = 'delivered' THEN total ELSE 0 END), so the aggregate only counts or adds up the rows you choose. It produces several category totals in one query.
Can CASE WHEN be used with GROUP BY?
Yes. Grouping by a CASE expression creates your own categories, such as salary bands, and lets you aggregate over them without a separate lookup table.
How does CASE WHEN handle NULL values?
Test for NULL with WHEN column IS NULL. WHEN column = NULL never matches. A CASE without ELSE also returns NULL for any row that matches no condition.
How do data analysts use CASE WHEN?
For customer and product segments, KPI buckets, readable labels for status codes, and conditional aggregation in revenue and churn reports, including the category fields that feed dashboards.
Is CASE WHEN important in SQL interviews?
Yes. Conditional aggregation with SUM or COUNT, CASE inside GROUP BY, the order of conditions and correct NULL handling are all common interview questions.