CASE — это «если, то» внутри запроса. Он проверяет условия сверху вниз, останавливается на первом истинном и возвращает соответствующее значение. Ключевое отличие от WHERE: CASE не отбирает строки, а вычисляет для каждой из них новое значение. Всё дальше снято на PostgreSQL 16.11.
Данные те же, что в остальных статьях справочника:
users
| id | name | city |
|---|---|---|
| 1 | Аня | Москва |
| 2 | Борис | Казань |
| 3 | Вера | Москва |
| 4 | Глеб | Казань |
orders
| id | user_id | amount | status | created_at |
|---|---|---|---|---|
| 10 | 1 | 900 | paid | 2026-05-28 |
| 11 | 1 | 2400 | paid | 2026-06-03 |
| 12 | 2 | 1200 | paid | 2026-06-07 |
| 13 | 2 | 1200 | cancelled | 2026-06-09 |
| 14 | 4 | 500 | paid | 2026-06-12 |
| 15 | 1 | 800 | paid | 2026-06-20 |
CASE останавливается на первом true
2400 первая ветка уже истинна: CASE возвращает «крупный» и не проверяет условия ниже.Как устроен CASE WHEN THEN ELSE END
Четыре ключевых слова и одно закрывающее. WHEN задаёт условие, THEN результат для него, ELSE запасной вариант, END завершает выражение:
SELECT id, amount,
CASE
WHEN amount >= 2000 THEN 'крупный'
WHEN amount >= 1000 THEN 'средний'
ELSE 'мелкий'
END AS bucket
FROM orders
ORDER BY id;
| id | amount | bucket |
|---|---|---|
| 10 | 900 | мелкий |
| 11 | 2400 | крупный |
| 12 | 1200 | средний |
| 13 | 1200 | средний |
| 14 | 500 | мелкий |
| 15 | 800 | мелкий |
END обязателен: без него запрос не разберётся. Алиас через AS не обязателен, но без него колонка получит имя case, что в отчёте выглядит как недоделка.
Ветвей WHEN может быть сколько угодно. Проверка идёт по порядку и заканчивается на первой сработавшей, остальные даже не рассматриваются.
Две формы: простая и поисковая
У CASE два синтаксиса. Поисковая форма пишет полное условие в каждой ветке, как выше. Простая форма выносит проверяемое выражение сразу после CASE и сравнивает его на равенство:
SELECT id, status,
CASE status
WHEN 'paid' THEN 'оплачен'
WHEN 'cancelled' THEN 'отменён'
ELSE 'неизвестно'
END AS status_ru
FROM orders
ORDER BY id;
| id | status | status_ru |
|---|---|---|
| 10 | paid | оплачен |
| 11 | paid | оплачен |
| 12 | paid | оплачен |
| 13 | cancelled | отменён |
| 14 | paid | оплачен |
| 15 | paid | оплачен |
Простая форма короче, но умеет только равенство: сравнений вроде >= или IS NULL в ней не выразить. Плюс ловушка. Ветка WHEN NULL не срабатывает никогда, потому что сравнение с NULL истинным не бывает, и на данных с пропусками простая форма молча выдаёт неверный ответ:
SELECT id, nullif(amount, 1200) AS amount,
CASE nullif(amount, 1200) WHEN NULL THEN 'пусто' ELSE 'есть' END AS short_form,
CASE WHEN nullif(amount, 1200) IS NULL THEN 'пусто' ELSE 'есть' END AS searched
FROM orders
ORDER BY id;
| id | amount | short_form | searched |
|---|---|---|---|
| 10 | 900 | есть | есть |
| 11 | 2400 | есть | есть |
| 12 | NULL | есть | пусто |
| 13 | NULL | есть | пусто |
| 14 | 500 | есть | есть |
| 15 | 800 | есть | есть |
Колонка short_form уверенно врёт: пустые значения помечены как «есть». Поисковая форма с IS NULL отвечает верно. Подробно про то, почему сравнение с пропуском не работает, в статье про NULL и трёхзначную логику.
Почему порядок веток решает результат
Первое истинное условие побеждает, поэтому широкая проверка, поставленная выше узкой, забирает себе все строки. Переставим два WHEN из первого примера местами:
SELECT id, amount,
CASE
WHEN amount >= 1000 THEN 'средний'
WHEN amount >= 2000 THEN 'крупный'
ELSE 'мелкий'
END AS bucket
FROM orders
ORDER BY id;
| id | amount | bucket |
|---|---|---|
| 10 | 900 | мелкий |
| 11 | 2400 | средний |
| 12 | 1200 | средний |
| 13 | 1200 | средний |
| 14 | 500 | мелкий |
| 15 | 800 | мелкий |
Заказ на 2400 стал «средним», а метка «крупный» не досталась никому: любая сумма от 2000 сначала проходит проверку «от 1000» и выходит из CASE там. Ошибки нет, запрос отработал, отчёт неверен.
Правило простое: условия пишут от узкого к широкому, от большего порога к меньшему. Если границы диапазонов пересекаются, порядок веток становится частью логики, а не оформлением.
Что вернёт CASE без ELSE
NULL. Ветка ELSE не обязательна, и её отсутствие означает «во всех остальных случаях ничего»:
SELECT id, amount,
CASE WHEN amount >= 2000 THEN 'крупный' END AS bucket
FROM orders
ORDER BY id;
| id | amount | bucket |
|---|---|---|
| 10 | 900 | NULL |
| 11 | 2400 | крупный |
| 12 | 1200 | NULL |
| 13 | 1200 | NULL |
| 14 | 500 | NULL |
| 15 | 800 | NULL |
Иногда это ровно то, что нужно, а иногда пустые ячейки в отчёте появляются именно отсюда. Дальше такой NULL поедет в агрегаты и в сравнения со всеми последствиями, поэтому ELSE лучше писать явно даже там, где кажется, что он не нужен.
Все ветки должны возвращать один тип
База выводит один общий тип для всего выражения, и несовместимые ветки не проходят:
SELECT id, CASE WHEN amount >= 2000 THEN 'крупный' ELSE 0 END AS bucket
FROM orders
ORDER BY id;
ERROR: invalid input syntax for type integer: "крупный"
LINE 1: SELECT id, CASE WHEN amount >= 2000 THEN 'крупный' ELSE 0 EN...
^
Сообщение сбивает с толку: кажется, что проблема в строке «крупный», хотя настоящая причина в соседней ветке с нулём. PostgreSQL решил, что колонка целочисленная, и попытался привести к числу текст. Смешивать число и текст в ветках нельзя, нужен либо '0' строкой, либо число вместо слова.
Сводная таблица одним запросом
Главное практическое применение CASE в аналитике: развернуть строки в колонки. Агрегат считает только те строки, которые прошли условие, остальным подставляется ноль:
SELECT u.city,
sum(CASE WHEN o.status = 'paid' THEN o.amount ELSE 0 END) AS paid,
sum(CASE WHEN o.status = 'cancelled' THEN o.amount ELSE 0 END) AS cancelled,
count(CASE WHEN o.status = 'paid' THEN 1 END) AS paid_orders
FROM users u
JOIN orders o ON o.user_id = u.id
GROUP BY u.city
ORDER BY u.city;
| city | paid | cancelled | paid_orders |
|---|---|---|---|
| Казань | 1700 | 1200 | 2 |
| Москва | 4100 | 0 | 3 |
Один проход по данным вместо трёх запросов. В PostgreSQL то же самое пишется короче через FILTER:
SELECT u.city,
sum(o.amount) FILTER (WHERE o.status = 'paid') AS paid,
sum(o.amount) FILTER (WHERE o.status = 'cancelled') AS cancelled,
count(*) FILTER (WHERE o.status = 'paid') AS paid_orders
FROM users u
JOIN orders o ON o.user_id = u.id
GROUP BY u.city
ORDER BY u.city;
| city | paid | cancelled | paid_orders |
|---|---|---|---|
| Казань | 1700 | 1200 | 2 |
| Москва | 4100 | NULL | 3 |
Числа те же, кроме одной ячейки. У Москвы нет отменённых заказов: CASE с ELSE 0 просуммировал нули и дал 0, а FILTER суммировал пустой набор и дал NULL. Оба ответа корректны, но в отчёт обычно нужен ноль, поэтому FILTER заворачивают в coalesce. Зато FILTER короче и переносится хуже: за пределами PostgreSQL его чаще всего нет.
Внутри count прячется отдельная ловушка. Ветку ELSE 0 там ставить нельзя:
SELECT u.city,
count(CASE WHEN o.status = 'paid' THEN 1 END) AS right_way,
count(CASE WHEN o.status = 'paid' THEN 1 ELSE 0 END) AS wrong_way
FROM users u
JOIN orders o ON o.user_id = u.id
GROUP BY u.city
ORDER BY u.city;
| city | right_way | wrong_way |
|---|---|---|
| Казань | 2 | 3 |
| Москва | 3 | 3 |
В Казани три заказа, из них оплачено два. Правая колонка насчитала три, потому что count считает все непустые значения, а ноль — вполне себе значение. Без ELSE неподходящие строки дают NULL, и счётчик их пропускает. Для sum всё наоборот: там ELSE 0 уместен, потому что ноль в сумме ничего не меняет. Разница между count(*) и count(колонка) подробно разобрана в статье про GROUP BY и HAVING.
CASE в WHERE, ORDER BY и GROUP BY
CASE возвращает значение, поэтому работает везде, где значение уместно. В ORDER BY он задаёт свой порядок вместо алфавитного:
SELECT id, status, amount FROM orders
ORDER BY CASE status WHEN 'paid' THEN 0 ELSE 1 END, amount DESC, id;
| id | status | amount |
|---|---|---|
| 11 | paid | 2400 |
| 12 | paid | 1200 |
| 10 | paid | 900 |
| 15 | paid | 800 |
| 14 | paid | 500 |
| 13 | cancelled | 1200 |
Оплаченные заказы подняты наверх, внутри группы порядок по сумме. Так выносят наверх приоритетные статусы, не заводя отдельной колонки. Остальные приёмы сортировки собраны в статье про ORDER BY.
В WHERE CASE задаёт разные правила фильтрации для разных строк:
SELECT id, amount, status FROM orders
WHERE CASE WHEN status = 'cancelled' THEN amount > 5000 ELSE amount > 1000 END
ORDER BY id;
| id | amount | status |
|---|---|---|
| 11 | 2400 | paid |
| 12 | 1200 | paid |
Порог для отменённых заказов выше, поэтому отменённый на 1200 не прошёл, а оплаченный на ту же сумму прошёл. Читается такая конструкция тяжело, и почти всегда её можно переписать через OR с явными скобками.
В GROUP BY CASE создаёт группы, которых нет в данных:
SELECT CASE WHEN amount >= 1000 THEN 'от 1000' ELSE 'до 1000' END AS bucket,
count(*) AS cnt, sum(amount) AS total
FROM orders
GROUP BY 1
ORDER BY 1;
| bucket | cnt | total |
|---|---|---|
| до 1000 | 3 | 2200 |
| от 1000 | 3 | 4800 |
Здесь GROUP BY 1 ссылается на первую колонку выборки, чтобы не повторять всё выражение целиком.
На чём ловят на собеседовании
«Порядок веток не важен, сработает самое точное условие». Сработает первое истинное. Широкое условие выше узкого забирает все строки, и метка из нижней ветки не достанется никому.
«CASE ... WHEN NULL поймает пустые значения». Не поймает ни одной строки. Нужна поисковая форма с IS NULL.
«Без ELSE строка выпадет из результата». Не выпадет, CASE вернёт ей NULL. Отбором строк занимается WHERE, а CASE только вычисляет значение.
«count(CASE WHEN ... THEN 1 ELSE 0 END) посчитает подходящие». Посчитает все, потому что ноль тоже значение. Ветку ELSE в count не пишут.
«CASE защищает от деления на ноль». На простом запросе действительно защищает: CASE WHEN amount <> 1200 THEN amount / (amount - 1200) END отрабатывает без ошибки. Но документация PostgreSQL прямо предупреждает, что порядок вычисления подвыражений не гарантирован и полагаться на такую защиту нельзя. Надёжный способ — nullif(amount - 1200, 0): деление на NULL даёт NULL вместо падения.
Частые вопросы
Чем CASE отличается от IF
IF в стандартном SQL нет, есть только CASE. В MySQL существует функция IF(условие, тогда, иначе), а в SQL Server IIF, но обе привязаны к своей базе. CASE работает везде.
Можно ли вкладывать CASE друг в друга
Да, CASE внутри THEN или ELSE разрешён. Читается такое тяжело, и обычно вложенность разворачивается в дополнительные WHEN на одном уровне: условия просто становятся составными через AND.
Как в CASE обработать несколько условий сразу
Обычными логическими операторами внутри WHEN: WHEN status = 'paid' AND amount >= 1000 THEN .... Отдельного синтаксиса для этого нет.
Почему CASE не даёт отфильтровать строки
Потому что это выражение, а не фильтр. Он вычисляет значение для каждой строки, а решает, показывать её или нет, только WHERE. Обернуть CASE в WHERE можно, если он возвращает логическое значение.
Где потренироваться
В пути «SQL для аналитиков» на Koddo CASE появляется в паке про агрегаты: сегментация заказов по сумме, сводка по статусам одним запросом, приоритетная сортировка. Собранные вместе сюжеты лежат в задачах по SQL с решениями, а проверить себя на живой задаче можно на поиске повторных откликов.