CASE: Decisions Inside Queries

Beginner · 18 min read · ▶ live playground · ✦ checkpoint

CASE lets a SQL query make a decision and return a different value for each row. It is the expression you reach for when raw values need labels, statuses, custom sort rules, or row-by-row signals for later summaries.

The shape looks wordy at first, but it reads like a small checklist:

SELECT name,
       price,
       CASE
         WHEN price < 3.00 THEN 'budget'
         WHEN price < 4.00 THEN 'everyday'
         ELSE 'treat'
       END AS price_band
FROM menu_items
ORDER BY price, name;
      name       | price | price_band
-----------------+-------+------------
 Sparkling Water |  2.20 | budget
 Espresso        |  2.50 | budget
 Earl Grey       |  3.00 | everyday
 Croissant       |  3.20 | everyday
 Sencha          |  3.40 | everyday
 Banana Bread    |  3.60 | everyday
 Cappuccino      |  3.80 | everyday
 Flat White      |  4.00 | treat
 Cinnamon Roll   |  4.10 | treat
 Cold Brew       |  4.50 | treat
(10 rows)

This is a searched CASE: each WHEN has a full condition. Postgres checks the branches from top to bottom. The first condition that is TRUE wins, and its THEN value becomes the value of the whole CASE expression for that row. If none match, ELSE supplies the fallback.

Notice the boundary points. Earl Grey is exactly 3.00, so it is not < 3.00; it falls to the second branch. Flat White is exactly 4.00, so it is not < 4.00; it falls to ELSE. CASE is not fuzzy. Your inequalities are the business rule.

CASE returns one value per row

CASE is an expression, just like price * 12 or name || ' (' || category || ')'. That means it can sit in the SELECT list, get an alias, and become a new column in the result grid. It does not filter rows by itself. It labels the rows you already selected.

Because CASE uses ordinary conditions, the NULL rule from the previous lesson applies: only TRUE wins. FALSE does not win, and UNKNOWN does not win. If you want a missing value to get its own label, say that with IS NULL:

SELECT name,
       bonus,
       CASE
         WHEN bonus IS NULL THEN 'undecided'
         WHEN bonus = 0 THEN 'none'
         WHEN bonus < 200 THEN 'small'
         ELSE 'standard'
       END AS bonus_status
FROM staff
ORDER BY id;
 name  | bonus  | bonus_status
-------+--------+--------------
 Rosa  |   NULL | undecided
 Ken   | 400.00 | standard
 Ada   | 120.00 | small
 Tomas |   NULL | undecided
 Yuki  | 250.00 | standard
 Priya |   0.00 | none
(6 rows)

Without that first branch, Rosa and Tomas would not match bonus = 0 or bonus < 200; those conditions would be UNKNOWN. If a CASE has no ELSE and no branch wins, the result is NULL. That can be useful, but it should be deliberate. Beginners often forget the fallback and accidentally create missing values.

Simple CASE: exact matches on one expression

When every branch asks "what exact value is this one expression?", the shorter form reads better:

SELECT name,
       category,
       CASE category
         WHEN 'coffee' THEN 'bar'
         WHEN 'tea' THEN 'bar'
         WHEN 'pastry' THEN 'bakery'
         ELSE 'front case'
       END AS station
FROM menu_items
ORDER BY category, name;
      name       | category |  station
-----------------+----------+------------
 Cappuccino      | coffee   | bar
 Cold Brew       | coffee   | bar
 Espresso        | coffee   | bar
 Flat White      | coffee   | bar
 Sparkling Water | drinks   | front case
 Banana Bread    | pastry   | bakery
 Cinnamon Roll   | pastry   | bakery
 Croissant       | pastry   | bakery
 Earl Grey       | tea      | bar
 Sencha          | tea      | bar
(10 rows)

Read it as: "look at category; when it is 'coffee', return 'bar'; when it is 'tea', return 'bar'; otherwise keep going." Simple CASE is great for code-to-label mappings, category cleanup, and small exact-value translations.

Use searched CASE when you need inequalities, IS NULL, IN, AND/OR, or different columns in different branches. Use simple CASE when one expression is being compared to exact values over and over. The result is the same kind of thing — one expression that returns one value per row — but the cleaner spelling changes with the problem.

Custom sort orders

ORDER BY can sort by an expression, and CASE is an expression. That makes it the usual way to sort by a business order instead of alphabetic order.

Suppose the menu should print coffee first, then pastries, then tea, then everything else:

SELECT name, category, price
FROM menu_items
ORDER BY CASE category
           WHEN 'coffee' THEN 1
           WHEN 'pastry' THEN 2
           WHEN 'tea' THEN 3
           ELSE 4
         END,
         price,
         name;
      name       | category | price
-----------------+----------+-------
 Espresso        | coffee   |  2.50
 Cappuccino      | coffee   |  3.80
 Flat White      | coffee   |  4.00
 Cold Brew       | coffee   |  4.50
 Croissant       | pastry   |  3.20
 Banana Bread    | pastry   |  3.60
 Cinnamon Roll   | pastry   |  4.10
 Earl Grey       | tea      |  3.00
 Sencha          | tea      |  3.40
 Sparkling Water | drinks   |  2.20
(10 rows)

Those numbers do not have to appear in the grid. They are just the invisible sort keys: coffee gets 1, pastry gets 2, tea gets 3, drinks fall to 4. The next sort keys, price and name, make the order stable inside each group.

A preview of conditional counting

Later, aggregate functions will collapse many rows into totals. CASE is one of the ways you prepare those totals because it can turn a condition into a per-row marker:

SELECT name,
       category,
       CASE
         WHEN category = 'coffee' THEN 1
         ELSE 0
       END AS coffee_marker
FROM menu_items
ORDER BY id;
      name       | category | coffee_marker
-----------------+----------+---------------
 Espresso        | coffee   |             1
 Cappuccino      | coffee   |             1
 Flat White      | coffee   |             1
 Cold Brew       | coffee   |             1
 Earl Grey       | tea      |             0
 Sencha          | tea      |             0
 Croissant       | pastry   |             0
 Cinnamon Roll   | pastry   |             0
 Banana Bread    | pastry   |             0
 Sparkling Water | drinks   |             0
(10 rows)

Do not worry about the counting part yet. The important idea is that CASE can manufacture the row-by-row signal: coffee rows get 1, every other row gets 0. Section 4 will show how aggregates use those signals.

sql — playgroundlive
⌘/Ctrl + Enter to run

CASE lets a query make decisions; casts give it explicit type conversions. Next, you will learn when SQL converts values for you, when it refuses, and when the convenient-looking conversion is the bug.

Checkpoint

Answer all three to mark this lesson complete

+50 XP on completion