SQLab Hub
Lesson 26 · CASE WHEN
Lesson 26 · Functions

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.

⑂ IF / ELSE logic in SQL ∑ Conditional aggregation ▶ Runnable Playground examples 🧪 6 practice exercises 📝 29-question quiz ⏱ ~35 min

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:

SQLite · runs in the Playground
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

Students graded with CASE WHEN
namemarksgrade
Ishaan94A
Divya81B
Karan67C
Sneha45Fail
Arjun60C
✓ 5 rows · real output from the Playground database

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 questionCASE WHEN answer
Label employees by salary bandWHEN salary >= 150000 THEN 'High'
Turn status codes into readable textCASE status WHEN 'delivered' THEN 'Complete' ...
Count orders per status in one rowSUM(CASE WHEN status = 'shipped' THEN 1 ELSE 0 END)
Sort by business priority, not alphabeticallyORDER BY CASE status WHEN 'pending' THEN 1 ... END
Flag missing dataWHEN shipped_date IS NULL THEN 'Not shipped'

CASE WHEN Syntax

SQL · syntax
CASE
  WHEN condition_1 THEN result_1
  WHEN condition_2 THEN result_2
  ...
  ELSE default_result
END
PartWhat it does
CASEStarts the expression
WHEN conditionA test that is true or false for the row, such as salary >= 150000
THEN resultThe value returned when that test is the first true one
ELSE defaultOptional. The value when no condition is true; without it the result is NULL
ENDCloses 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:

SQLite · runs in the Playground
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

Employees labelled by salary band
first_namelast_namesalarysalary_band
WilliamLewis61774Low
JohnMiller191694High
DanielRivera135067Medium
MelissaSmith51465Low
JasonMitchell156865High
AmyNguyen194543High
✓ 6 rows · real output from the Playground database

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:

SQLite · runs in the Playground
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

Employees grouped with AND and IN conditions
first_namedepartmenthire_dateemployee_group
DanielEngineering2016-06-06Senior engineer
DonnaEngineering2018-06-21Engineer
ChrisEngineering2019-03-07Engineer
AnthonyEngineering2019-04-04Engineer
BarbaraEngineering2019-12-21Engineer
SusanEngineering2020-05-19Engineer
DorothyEngineering2021-04-21Engineer
MelissaEngineering2022-04-13Engineer
✓ 8 rows · real output from the Playground database

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:

SQLite · runs in the Playground
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

Order statuses translated with a simple CASE
statuscustomer_labelorders
deliveredComplete82
processingWaiting28
cancelledClosed27
pendingWaiting23
shippedOn the way21
refundedClosed19
✓ 6 rows · real output from the Playground database
Simple CASESearched CASE
FormCASE status WHEN 'shipped' THEN ...CASE WHEN status = 'shipped' THEN ...
Can testEquality with one expressionAny condition: ranges, LIKE, IS NULL, several columns
Use it forTranslating codes into labelsEverything else
Simple CASE cannot find NULL. 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:

SQLite · runs in the Playground
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

Bonus calculated from the hire date
first_namehire_datesalarybonus
William2022-07-24617741235
John2021-08-091916949585
Daniel2016-06-0613506713507
Melissa2022-04-13514651029
Jason2019-10-171568657843
Amy2021-01-161945439727
✓ 6 rows · real output from the Playground database

The same pattern gives tiered shipping fees on orders:

SQLite · runs in the Playground
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

Shipping fee by order total
idtotalshipping_fee
12638.110
229470
3683.249.99
43920.440
51120.990
61327.010
✓ 6 rows · real output from the Playground database

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:

SQLite · runs in the Playground
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

Orders sorted by status in business order
idstatusorder_date
70pending2024-12-24
168pending2024-11-01
112pending2024-07-06
76pending2024-03-14
147pending2024-03-08
129pending2024-02-17
193pending2023-12-11
72pending2023-11-08
✓ 8 rows · real output from the Playground database

In WHERE, CASE lets the threshold depend on the row. Here Sales employees qualify above 150,000 and everyone else above 190,000:

SQLite · runs in the Playground
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

Employees above a department-dependent threshold
first_namedepartmentsalary
StevenMarketing204449
NancyCustomer Success203999
JohnOperations203467
DonnaEngineering199029
MatthewMarketing196530
PaulFinance196032
LauraSales195759
DorothyEngineering195690
LindaFinance194604
AmyDesign194543
✓ 18 rows · first 10 shown · real output from the Playground database

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:

SQLite · runs in the Playground
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

Employees per salary band
salary_bandemployeesavg_salary
High40180517
Medium43118566
Low3770419
✓ 3 rows · real output from the Playground database

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:

SQLite · runs in the Playground
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

Orders per year split by status
yearall_ordersdeliveredcancelled_or_refundeddelivered_revenue
202262241365652.19
202372302170494.51
202466281274629.64
✓ 3 rows · real output from the Playground database

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:

SQLite · runs in the Playground
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

High earners and employees on leave per department
departmentemployeeshigh_earnerson_leave
Customer Success1572
Operations1363
Design1051
Engineering1140
Marketing1340
Finance1030
HR1233
Legal1634
Sales1130
Product921
✓ 10 rows · real output from the Playground database

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):

SQLite · runs in the Playground
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

Orders placed on weekdays and at weekends
day_typeordersavg_order_value
Weekday1372436.85
Weekend632811.46
✓ 2 rows · real output from the Playground database

A company whose financial year starts in April needs its own quarter labels. CASE maps calendar months to fiscal quarters:

SQLite · runs in the Playground
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

Orders per fiscal quarter starting in April
fiscal_quarterorders
Q1 (Apr-Jun)46
Q2 (Jul-Sep)45
Q3 (Oct-Dec)59
Q4 (Jan-Mar)50
✓ 4 rows · real output from the Playground database

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:

SQLite · runs in the Playground
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

Orders with and without a shipped date
shipping_stateorders
Not shipped97
Shipped103
✓ 2 rows · real output from the Playground database

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:

SQLite · runs in the Playground
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

Users per email provider
providerusers
Microsoft48
Other45
Yahoo29
Gmail28
✓ 4 rows · real output from the Playground database

CASE WHEN With Subqueries

A condition can compare with the result of a subquery. This labels each employee against the company-wide average salary:

SQLite · runs in the Playground
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

Employees compared with the average salary
first_namedepartmentsalarycompared_with_company
WilliamCustomer Success61774At or below average
JohnLegal191694Above average
DanielEngineering135067Above average
MelissaEngineering51465At or below average
JasonDesign156865Above average
AmyDesign194543Above average
✓ 6 rows · real output from the Playground database

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:

SQL · nested
-- Nested
CASE WHEN status = 'discontinued' THEN 'Discontinued'
     ELSE CASE WHEN stock < 50 THEN 'Reorder now'
               ELSE 'In stock'
          END
END
SQLite · runs in the Playground
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

Products that need attention
namestatusstockstock_label
The Manager's Pathdiscontinued62Discontinued
Succulent Plant Setdiscontinued107Discontinued
Retinol Eye Cream 30mldiscontinued227Discontinued
Sticky Notes Assorted 18pkdiscontinued285Discontinued
Artisan Hot Sauce Triodiscontinued391Discontinued
4-Person Camping Tentdiscontinued394Discontinued
Smart Thermostatdiscontinued401Discontinued
Leather Steering Wheel Coveractive10Reorder now
Giant Stuffed Teddy Bearactive12Reorder now
Winter Parka Jacketactive33Reorder now
✓ 11 rows · first 10 shown · real output from the Playground database

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

FeatureMySQLPostgreSQLSQL ServerSQLite
Searched and simple CASEYesYesYesYes
Two-way shortcutIF(cond, a, b)NoneIIF(cond, a, b)IIF(cond, a, b) (3.32+)
FILTER on aggregatesNoYesNoYes (3.30+)
GROUP BY a SELECT aliasYesYesNoYes

IIF and FILTER are shorter ways to write the same thing. In the Playground both work:

SQLite · runs in the Playground
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

FILTER, CASE and IIF give the same count
delivered_filterdelivered_casedelivered_iif
828282
✓ 1 row · real output from the Playground database

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

❌ Syntax error
SQL
SELECT first_name,
       CASE WHEN salary >= 150000 THEN 'High'
FROM employees;
✅ Closed with END
SQL
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

❌ Nobody is ever "High"
SQL
CASE
  WHEN salary >= 90000  THEN 'Medium'
  WHEN salary >= 150000 THEN 'High'
  ELSE 'Low'
END
✅ Highest threshold first
SQL
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

❌ Unmatched rows become NULL
SQL
CASE WHEN department = 'Sales' THEN 'Sales team' END
✅ Explicit fallback
SQL
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 =

❌ Every row says "Shipped"
SQL
CASE WHEN shipped_date = NULL THEN 'Not shipped'
     ELSE 'Shipped' END
✅ Use IS NULL
SQL
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

❌ Text in one branch, number in another
SQL
CASE WHEN salary >= 150000 THEN 'High' ELSE 0 END
✅ One type throughout
SQL
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 >=

❌ Exactly 90,000 is "Low"
SQL
CASE WHEN salary > 90000 THEN 'Medium' ELSE 'Low' END
✅ Matches the rule "90,000 and above"
SQL
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:

WhereEffectWhat to do
WHEREA column wrapped in CASE usually cannot use an index, just like a column wrapped in a functionRewrite as plain AND / OR conditions when you can
GROUP BYThe CASE runs for every row before groupingFine for most tables; on very large ones store the category in a column
Many conditional aggregatesOne pass over the table, however many columnsUsually faster than several separate queries

Use EXPLAIN to check whether a query uses an index before rewriting it.

CASE WHEN Cheat Sheet

PatternExample
Label by rangeCASE WHEN salary >= 150000 THEN 'High' WHEN salary >= 90000 THEN 'Medium' ELSE 'Low' END
Translate codesCASE status WHEN 'shipped' THEN 'On the way' ELSE 'Other' END
Count matching rowsSUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END)
Total matching rowsSUM(CASE WHEN status = 'delivered' THEN total ELSE 0 END)
Count without ELSECOUNT(CASE WHEN salary >= 150000 THEN 1 END)
Custom sortORDER BY CASE status WHEN 'pending' THEN 1 WHEN 'shipped' THEN 2 ELSE 3 END
Missing valuesCASE WHEN shipped_date IS NULL THEN 'Not shipped' ELSE 'Shipped' END
Own categoriesGROUP 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.

  1. 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: products

    Show solution
    SQLite · runs in the Playground
    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.

  2. 2Easy Classify reviews as "Positive" (4-5 stars), "Neutral" (3) or "Negative" (1-2) and count each group. Table: reviews

    Show solution
    SQLite · runs in the Playground
    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.

  3. 3Medium For each payment method, show the number of payments and how many of them succeeded and failed, in one row per method. Table: payments

    Show solution
    SQLite · runs in the Playground
    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.

  4. 4Medium Count premium and regular users per country, side by side. Table: users

    Show solution
    SQLite · runs in the Playground
    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.

  5. 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: orders

    Show solution
    SQLite · runs in the Playground
    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().

  6. 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: employees

    Show solution
    SQLite · runs in the Playground
    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

Question 1 of 29

What is the main purpose of CASE WHEN in SQL?

Question 2 of 29

Which keyword ends every CASE expression?

Question 3 of 29

What happens if no WHEN condition matches and there is no ELSE?

Question 4 of 29

In what order are the WHEN conditions checked?

Question 5 of 29

For salary = 160000, what does CASE WHEN salary >= 50000 THEN 'Medium' WHEN salary >= 100000 THEN 'High' ELSE 'Low' END return?

Question 6 of 29

Is CASE WHEN similar to IF / ELSE in programming?

Question 7 of 29

Can a CASE expression be used inside SUM()?

Question 8 of 29

What does COUNT(CASE WHEN department = 'Sales' THEN 1 END) count?

Question 9 of 29

Can you GROUP BY a CASE expression?

Question 10 of 29

How do you test for NULL inside CASE?

Question 11 of 29

What is conditional aggregation?

Question 12 of 29

Can one CASE be nested inside another?

Question 13 of 29

Where can a CASE expression appear?

Question 14 of 29

What is CASE in ORDER BY typically used for?

Question 15 of 29

What is the rule for the data types of THEN and ELSE?

Question 16 of 29

How does a simple CASE (CASE col WHEN value ...) differ from a searched CASE?

Question 17 of 29

Can CASE WHEN be used in a WHERE clause?

Question 18 of 29

What is CASE WHEN combined with LIKE commonly used for?

Question 19 of 29

Why is CASE WHEN so common in analytics work?

Question 20 of 29

Is CASE WHEN a common SQL interview topic?

Question 21 of 29

What does SELECT name, CASE WHEN rating >= 4 THEN 'Positive' WHEN rating = 3 THEN 'Neutral' ELSE 'Negative' END FROM reviews classify?

Question 22 of 29

What is the best practice for nested CASE expressions?

Question 23 of 29

What does CASE WHEN phone IS NULL THEN 'Missing' ELSE 'Available' END detect?

Question 24 of 29

What does "wrong business logic" in a CASE expression usually mean?

Question 25 of 29

What is a performance risk of CASE inside WHERE?

Question 26 of 29

How does CASE WHEN syntax differ between MySQL, SQL Server and PostgreSQL?

Question 27 of 29

What can EXPLAIN tell you about a query with CASE in WHERE?

Question 28 of 29

How should you test a CASE expression?

Question 29 of 29

What separates CASE WHEN from a stored procedure's IF statement?

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.