Tier 2 · Core SQL

CASE WHEN Expressions in SQL

By Owen Middleton · Updated September 2026 · Examples run on PostgreSQL 17

What is CASE WHEN in SQL?

CASE WHEN evaluates a list of conditions in order and returns the result from the first one that matches.

You're building a product report and the catalog team wants a pricing tier column on every row. Products above $500 should be labeled 'premium,' everything else 'standard.' Instead of exporting to a spreadsheet and adding the column manually, you compute it directly in the query. Any time you need to attach a classification, a status label, or a conditional value to each row, CASE WHEN is how you do it.

The expression checks each WHEN branch against the current row, top to bottom, and returns the THEN value for the first condition that's true. Once it finds a match, it stops. The rest of the branches don't run.

How do you write a CASE WHEN expression in SQL?

Here's the pricing tier example:

SELECT name, price,
  CASE WHEN price > 500 THEN 'premium'
       ELSE 'standard'
  END AS price_tier
FROM products

Every row gets a price_tier value. ELSE is the fallback for any row where no WHEN condition matched.

Does the order of WHEN branches matter in SQL?

You can stack as many WHEN branches as you need. The ordering matters: SQL returns on the first match and stops. Write the most specific conditions first, the most general last:

SELECT id, total_amount,
  CASE WHEN total_amount > 1500 THEN 'premium'
       WHEN total_amount > 500  THEN 'high'
       ELSE 'standard'
  END AS tier
FROM orders

A $1,600 order hits the first branch and gets 'premium.' A $700 order passes the first branch and gets 'high.' Anything under $500 falls through to ELSE. Swap the first two branches and a $1,600 order would return 'high' instead — the order of WHEN branches is the order of evaluation.

CASE WHEN can also compute values, not just labels:

SELECT id, total_amount,
  CASE WHEN total_amount > 800 THEN total_amount * 0.9
       ELSE total_amount
  END AS adjusted_total
FROM orders

High-value orders get 10% knocked off; everything else is unchanged.

How do you handle NULL inside a CASE expression?

Handling NULL requires an explicit branch. A WHEN condition that compares against NULL using = produces NULL, which SQL treats as false. The condition fails silently and the row falls to the next branch. If NULL in a column should map to a specific label, write an IS NULL branch and place it first:

SELECT name,
  CASE WHEN category_id IS NULL THEN 'uncategorized'
       WHEN category_id >= 5    THEN 'specialty'
       ELSE 'general'
  END AS classification
FROM products

The IS NULL branch comes first because any row with NULL would silently pass through the numeric comparison branches without matching. Placing it first catches NULL before any other condition runs.

What does CASE return when nothing matches and there is no ELSE?

The one thing that trips people up: omitting ELSE.

If no condition matches and there's no ELSE, CASE returns NULL. Not an empty string. Not an error. NULL. That NULL flows silently into the output and produces unexpected results wherever the column is used. Writing an explicit ELSE makes the fallback behavior visible in the code instead of implied by omission — even if the fallback is just 'other' or 'unknown.'

Practice CASE WHEN in SQL

Practice · easy ecommerce · Brightlane

Brightlane's product team is preparing a pricing report and needs every item in the catalogue labelled by price tier.

Write a query to return each product's name, its price, and a price_tier label:

  • 'premium' if the price is above $500.
  • 'standard' for all other prices.

Assumptions:

  • The products table contains every product in Brightlane's catalogue.
  • A product priced exactly at $500 is 'standard' (the threshold is strictly greater-than).

Output:

  • One row per product, with columns name, price, and price_tier.
Schema · ecommerce5 tables? = nullable
categories
idinteger
nametext
parent_id?integer
products
idinteger
nametext
category_id?integer
pricenumeric
stock_qtyinteger
attributes?jsonb
order_items
idinteger
order_id?integer
product_id?integer
quantityinteger
unit_pricenumeric
customers
idinteger
nametext
emailtext
city?text
countrytext
created_attimestamptz
is_activeboolean
orders
idinteger
customer_id?integer
ordered_attimestamptz
statustext
total_amountnumeric

Run previews · Check grades

Write a query, then run it to see results here.

Worked solution

The full breakdown walks through the shape, each clause, why this approach beats the alternatives, and the trap to avoid.

See the full worked solution

10 CASE WHEN practice problems

Start learning to practice all 10 CASE WHEN problems, with instant grading and mastery tracking.

Deeper guides on CASE WHEN

Common questions about CASE WHEN

What happens if two WHEN branches are both true?

The first one wins, and the branches below it are not evaluated for that row. CASE tests its branches from the top and stops at the first match, which is why the most specific condition belongs first and a general catch-all belongs last. Two things escape that: a constant such as 1/0 can be evaluated when the query is planned, and an aggregate inside a branch is computed before any branch is chosen.

Can the branches of a CASE return different types?

Only when the types are compatible. PostgreSQL settles on one type for the whole expression, so an integer branch beside a decimal branch resolves to numeric, while a text branch beside a number raises an error instead of picking one.

Is there a shorter CASE form for comparing one value?

Yes. Put the expression straight after CASE and each WHEN then carries only the value to compare against, which reads better when every branch tests the same column for equality. Go back to the longer form as soon as a branch needs a range or a different column.

Can you use CASE in an ORDER BY?

Yes, and it is the usual way to impose a custom sort order. Map each value to a number inside the CASE and the rows arrive in the sequence you chose rather than alphabetically, which is how you get pending to sort before shipped.

How you actually get good at SQL

Reading explains SQL. Writing it, over and over with instant feedback, is what makes you fluent.

That's the whole SQLMaxx loop: 600+ real problems, instant AI feedback, mastery you can actually see, and spaced review that won't let you forget.

A stack of SQL practice problem cards, the top card showing an employees table.
615 problems · 66 concepts

Real problems. Not toy examples.

615 hand-built problems spanning all 66 concepts, from basic SELECTs to window functions, built on real schemas and real business questions, the kind you'll actually get asked on the job. Enough reps to make SQL automatic.

A retro computer showing a SQL query marked correct with a green checkmark.
Instant AI feedback

Write a query. Know if it's right in one second.

No copying an answer and hoping it clicked. The AI grader checks your real query against real data, catches exactly what's wrong, and explains the fix in plain English, like a senior analyst reading over your shoulder on every problem.

A circular mastery progress dial filling from blue to green, the SQLMaxx diamond at its center.
Mastery tracking

Stop guessing whether you actually know it.

SQLMaxx tracks every concept and shows you what you've mastered and what's still shaky. Your skills fill in one concept at a time, so 'I think I get joins' becomes something you can prove.

A SQL query editor circled by a blue return arrow with a clock, scheduled to come back for review.
Spaced review

Learn it once. Keep it for good.

Most of what you learn this week fades by next week. So when a concept comes due for review, SQLMaxx hands you a fresh problem to solve from a blank editor, not a flashcard to re-read. A research-backed spaced-repetition algorithm (FSRS) times each return for right before you'd forget, so your SQL is still there months later, when the interview or the job actually needs it.