CASE WHEN в SQL: условия в запросе с примерами

SQL Автор: Среда и версия: PostgreSQL 16.11
содержание

CASE — это «если, то» внутри запроса. Он проверяет условия сверху вниз, останавливается на первом истинном и возвращает соответствующее значение. Ключевое отличие от WHERE: CASE не отбирает строки, а вычисляет для каждой из них новое значение. Всё дальше снято на PostgreSQL 16.11.

Данные те же, что в остальных статьях справочника:

users

idnamecity
1АняМосква
2БорисКазань
3ВераМосква
4ГлебКазань

orders

iduser_idamountstatuscreated_at
101900paid2026-05-28
1112400paid2026-06-03
1221200paid2026-06-07
1321200cancelled2026-06-09
144500paid2026-06-12
151800paid2026-06-20
SQL / 01

CASE останавливается на первом true

CASE останавливается на первом true01 / вход 02 / операция 03 / результат amount = 2400 WHEN amount >= 2000 true → «крупный» следующий WHEN amount >= 1000 не проверяется ELSE запасной вариант не проверяется01 / вход02 / операция03 / результатamount = 2400WHEN amount >= 2000true → «крупный»следующий WHENamount >= 1000не проверяетсяELSEзапасной вариантне проверяется
Для суммы 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;
idamountbucket
10900мелкий
112400крупный
121200средний
131200средний
14500мелкий
15800мелкий

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;
idstatusstatus_ru
10paidоплачен
11paidоплачен
12paidоплачен
13cancelledотменён
14paidоплачен
15paidоплачен

Простая форма короче, но умеет только равенство: сравнений вроде >= или 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;
idamountshort_formsearched
10900естьесть
112400естьесть
12NULLестьпусто
13NULLестьпусто
14500естьесть
15800естьесть

Колонка 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;
idamountbucket
10900мелкий
112400средний
121200средний
131200средний
14500мелкий
15800мелкий

Заказ на 2400 стал «средним», а метка «крупный» не досталась никому: любая сумма от 2000 сначала проходит проверку «от 1000» и выходит из CASE там. Ошибки нет, запрос отработал, отчёт неверен.

Правило простое: условия пишут от узкого к широкому, от большего порога к меньшему. Если границы диапазонов пересекаются, порядок веток становится частью логики, а не оформлением.

Что вернёт CASE без ELSE

NULL. Ветка ELSE не обязательна, и её отсутствие означает «во всех остальных случаях ничего»:

SELECT id, amount,
       CASE WHEN amount >= 2000 THEN 'крупный' END AS bucket
FROM orders
ORDER BY id;
idamountbucket
10900NULL
112400крупный
121200NULL
131200NULL
14500NULL
15800NULL

Иногда это ровно то, что нужно, а иногда пустые ячейки в отчёте появляются именно отсюда. Дальше такой 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;
citypaidcancelledpaid_orders
Казань170012002
Москва410003

Один проход по данным вместо трёх запросов. В 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;
citypaidcancelledpaid_orders
Казань170012002
Москва4100NULL3

Числа те же, кроме одной ячейки. У Москвы нет отменённых заказов: 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;
cityright_waywrong_way
Казань23
Москва33

В Казани три заказа, из них оплачено два. Правая колонка насчитала три, потому что 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;
idstatusamount
11paid2400
12paid1200
10paid900
15paid800
14paid500
13cancelled1200

Оплаченные заказы подняты наверх, внутри группы порядок по сумме. Так выносят наверх приоритетные статусы, не заводя отдельной колонки. Остальные приёмы сортировки собраны в статье про 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;
idamountstatus
112400paid
121200paid

Порог для отменённых заказов выше, поэтому отменённый на 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;
bucketcnttotal
до 100032200
от 100034800

Здесь 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 с решениями, а проверить себя на живой задаче можно на поиске повторных откликов.

Источники