CASE: if, then, else in a query
CASE is SQL's way of saying "if this, then that, otherwise something else". It lets a query turn values into labels, sort rows into bands, and count different things in different columns. Once you know it, you will find a use for it in most reports.
The shape of CASE
SELECT name, price,
CASE
WHEN price >= 1000 THEN 'premium'
WHEN price >= 300 THEN 'mid-range'
ELSE 'budget'
END AS band
FROM products
ORDER BY price DESC;Result
| name | price | band |
|---|---|---|
| Pressure Cooker 3 L | 1450 | premium |
| Power Bank | 1299 | premium |
| Bedsheet Double | 899 | mid-range |
| School Bag | 750 | mid-range |
| Basmati Rice 5 kg | 650 | mid-range |
| Earphones | 499 | mid-range |
| Steel Bottle 1 L | 450 | mid-range |
| Extension Board | 399 | mid-range |
| Wall Clock | 349 | mid-range |
| Tea 500 g | 280 | budget |
| Toor Dal 1 kg | 160 | budget |
| Sunflower Oil 1 L | 145 | budget |
16 rows
Showing the first 12 of 16 rows.
A CASE starts with the word CASE and ends with END. In between:
- each
WHEN condition THEN resultis tested in order, from the top; - the first condition that is true wins, and the rest are skipped;
ELSEgives the result when no condition is true.
Because the first match wins, the order of the WHEN lines matters. A price of 1,450 is also greater than 300, but the first test catches it. Put the narrowest condition first.
CASE produces a value, so it can go anywhere a value can: in the SELECT list, in WHERE, in ORDER BY, inside a function.
Without ELSE you get NULL
If nothing matches and there is no ELSE, the result is NULL.
SELECT name, stock,
CASE WHEN stock = 0 THEN 'sold out' END AS note
FROM products
WHERE category = 'Electronics';Result
| name | stock | note |
|---|---|---|
| LED Bulb 9 W | 200 | NULL |
| Extension Board | 35 | NULL |
| Earphones | 0 | sold out |
| Power Bank | 18 | NULL |
4 rows
Writing an ELSE every time is a good habit, even if it only says ELSE 'other'. It shows that you thought about the remaining cases.
The short form
When every test compares the same column with a fixed value, name the column once after CASE.
SELECT id, status,
CASE status
WHEN 'delivered' THEN 'Done'
WHEN 'shipped' THEN 'On the way'
WHEN 'pending' THEN 'Waiting'
ELSE 'Stopped'
END AS meaning
FROM orders
LIMIT 6;Result
| id | status | meaning |
|---|---|---|
| 1 | delivered | Done |
| 2 | delivered | Done |
| 3 | cancelled | Stopped |
| 4 | delivered | Done |
| 5 | delivered | Done |
| 6 | delivered | Done |
6 rows
The short form can only test for equality. For ranges, NULL checks or anything with AND, use the full form.
Dealing with NULL
CASE can replace a missing value with a label. For this simple job COALESCE is shorter, but CASE lets you do more.
SELECT name,
CASE WHEN email IS NULL THEN 'ask for email' ELSE 'has email' END AS email_status
FROM customers
ORDER BY name
LIMIT 5;Result
| name | email_status |
|---|---|
| Arjun Mehta | ask for email |
| Asha Verma | has email |
| Imran Ali | has email |
| Joy Thomas | has email |
| Kabir Khan | has email |
5 rows
Grouping by a CASE
Label the rows, then count by label. This is how bands and brackets are reported.
SELECT CASE
WHEN salary >= 50000 THEN '50,000 and above'
WHEN salary >= 35000 THEN '35,000 to 49,999'
ELSE 'below 35,000'
END AS salary_band,
COUNT(*) AS staff
FROM employees
GROUP BY 1
ORDER BY MIN(salary) DESC;Result
| salary_band | staff |
|---|---|
| 50,000 and above | 2 |
| 35,000 to 49,999 | 4 |
| below 35,000 | 2 |
3 rows
GROUP BY 1 means "group by the first column of the SELECT list". It saves repeating the whole CASE. SQLite, MySQL and PostgreSQL accept it. In SQL Server and Oracle, repeat the CASE in the GROUP BY.
Conditional counting
This is the most valuable CASE pattern. Put a CASE inside an aggregate, and one query can count several different things side by side.
SELECT COUNT(*) AS all_orders,
SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) AS delivered,
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) AS cancelled,
SUM(CASE WHEN status IN ('pending', 'shipped') THEN 1 ELSE 0 END) AS in_progress
FROM orders;Result
| all_orders | delivered | cancelled | in_progress |
|---|---|---|---|
| 20 | 13 | 2 | 5 |
1 row
Each CASE turns a row into 1 or 0, and SUM adds the ones. Without CASE this would take four separate queries.
Combine it with GROUP BY and you get a cross table: one row per customer, one column per status.
SELECT c.name,
SUM(CASE WHEN o.status = 'delivered' THEN 1 ELSE 0 END) AS delivered,
SUM(CASE WHEN o.status = 'cancelled' THEN 1 ELSE 0 END) AS cancelled,
SUM(CASE WHEN o.status IN ('pending', 'shipped') THEN 1 ELSE 0 END) AS open
FROM customers c
JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name
ORDER BY c.name;Result
| name | delivered | cancelled | open |
|---|---|---|---|
| Asha Verma | 3 | 0 | 1 |
| Imran Ali | 2 | 0 | 0 |
| Joy Thomas | 1 | 0 | 0 |
| Kabir Khan | 1 | 1 | 1 |
| Neha Joshi | 0 | 1 | 1 |
| Priya Nair | 1 | 0 | 1 |
| Ravi Kumar | 3 | 0 | 0 |
| Sunita Rao | 1 | 0 | 1 |
| Vikram Das | 1 | 0 | 0 |
9 rows
Turning rows into columns like this is called pivoting.
Conditional totals
Return an amount in place of the 1 and you get totals split by condition.
SELECT p.category,
SUM(CASE WHEN o.status = 'delivered' THEN oi.quantity * p.price ELSE 0 END) AS delivered_sales,
SUM(CASE WHEN o.status = 'cancelled' THEN oi.quantity * p.price ELSE 0 END) AS cancelled_sales
FROM order_items oi
JOIN products p ON oi.product_id = p.id
JOIN orders o ON oi.order_id = o.id
GROUP BY p.category
ORDER BY p.category;Result
| category | delivered_sales | cancelled_sales |
|---|---|---|
| Electronics | 4814 | 1299 |
| Grocery | 4355 | 0 |
| Home | 2800 | 899 |
| Stationery | 2520 | 0 |
4 rows
A percentage
The average of ones and zeros is the share of ones. Multiply by 100 for a percentage. Note the 1.0, which keeps the decimals.
SELECT ROUND(100 * AVG(CASE WHEN status = 'delivered' THEN 1.0 ELSE 0 END), 1) AS percent_delivered
FROM orders;Result
| percent_delivered |
|---|
| 65.0 |
1 row
A custom sort order
Alphabetical order is rarely the order a business wants. CASE in ORDER BY gives each value a rank of your choosing.
SELECT id, status, order_date
FROM orders
ORDER BY CASE status
WHEN 'pending' THEN 1
WHEN 'shipped' THEN 2
WHEN 'delivered' THEN 3
ELSE 4
END,
order_date
LIMIT 8;Result
| id | status | order_date |
|---|---|---|
| 18 | pending | 2026-09-01 |
| 19 | pending | 2026-09-17 |
| 20 | pending | 2026-10-04 |
| 13 | shipped | 2026-05-19 |
| 17 | shipped | 2026-08-11 |
| 1 | delivered | 2026-01-05 |
| 2 | delivered | 2026-01-12 |
| 4 | delivered | 2026-02-02 |
8 rows
Pending orders, the ones needing attention, come first.
Avoiding division by zero
A CASE can guard a division.
SELECT name, price, stock,
CASE WHEN stock = 0 THEN NULL ELSE price * 1.0 / stock END AS price_per_unit_in_stock
FROM products
WHERE category = 'Grocery';Result
| name | price | stock | price_per_unit_in_stock |
|---|---|---|---|
| Basmati Rice 5 kg | 650 | 40 | 16.25 |
| Toor Dal 1 kg | 160 | 75 | 2.1333333333333333 |
| Sunflower Oil 1 L | 145 | 60 | 2.4166666666666665 |
| Tea 500 g | 280 | 0 | NULL |
4 rows
A shorter way is the function NULLIF(a, b), which returns NULL when the two values are equal: price / NULLIF(stock, 0). In SQLite and MySQL, dividing by zero quietly gives NULL anyway. PostgreSQL, SQL Server and Oracle stop with an error, so the guard matters there.
CASE in an UPDATE
A single UPDATE can change different rows in different ways.
UPDATE products
SET price = CASE
WHEN category = 'Grocery' THEN price + 5
WHEN category = 'Home' THEN price - 50
ELSE price
END;Result
Done. 16 rows changed.
Do not forget the ELSE price. Without it, every product outside the two categories would have its price set to NULL.
Common mistakes
- Forgetting
END. EveryCASEneeds one. - Putting a wide condition above a narrow one, so that the narrow one is never reached.
- Mixing types in the results, such as a number in one branch and text in another. Strict databases refuse.
- Leaving out
ELSEin anUPDATE.
Try it yourself
Exercise 1. Show each employee's name and a column level that says senior if the salary is 45000 or more and junior otherwise.
Show one answer
SELECT name, CASE WHEN salary >= 45000 THEN 'senior' ELSE 'junior' END AS level FROM employees;| name | level |
|---|---|
| Anita Kapoor | senior |
| Rohit Bansal | senior |
| Deepa Iyer | senior |
| Manoj Yadav | senior |
| Farah Sheikh | junior |
| Gopal Reddy | junior |
| Harpreet Kaur | junior |
| Nikhil Jain | junior |
8 rows
Exercise 2. In one row, show how many products are in stock (stock above 0) and how many are sold out. Two columns.
Show one answer
SELECT SUM(CASE WHEN stock > 0 THEN 1 ELSE 0 END), SUM(CASE WHEN stock = 0 THEN 1 ELSE 0 END) FROM products;| SUM(CASE WHEN stock > 0 THEN 1 ELSE 0 END) | SUM(CASE WHEN stock = 0 THEN 1 ELSE 0 END) |
|---|---|
| 14 | 2 |
1 row
Exercise 3. Show each product's name and a column stock_note: none when stock is 0, low when it is below 25, and fine otherwise.
Show one answer
SELECT name, CASE WHEN stock = 0 THEN 'none' WHEN stock < 25 THEN 'low' ELSE 'fine' END AS stock_note FROM products;| name | stock_note |
|---|---|
| Basmati Rice 5 kg | fine |
| Toor Dal 1 kg | fine |
| Sunflower Oil 1 L | fine |
| Tea 500 g | none |
| Notebook 200 pages | fine |
| Ball Pen pack of 10 | fine |
| Geometry Box | fine |
| School Bag | low |
| LED Bulb 9 W | fine |
| Extension Board | fine |
| Earphones | none |
| Power Bank | low |
16 rows
Showing the first 12 of 16 rows.
Examples on this page were run with SQLite 3.50.4 on 12 November 2026. The results shown are the real results.
This is an independent learning site. It is not connected with the makers of any database product. MySQL, PostgreSQL, SQL Server, Oracle and SQLite are named only so that you know where a piece of SQL works. The shop, its customers and its staff in the practice database are made up.