CASE WHEN — это способ написать условие внутри SQL-запроса.
Если совсем просто, это SQL-версия привычного if/else.
В обычном коде мы можем написать: если оценка больше или равна 90 — поставить A, если больше или равна 70 — поставить B, иначе — F.
В SQL такая логика записывается через CASE:
SELECT
student_name,
score,
CASE
WHEN score >= 90 THEN 'A'
WHEN score >= 70 THEN 'B'
WHEN score >= 50 THEN 'C'
ELSE 'F'
END AS grade
FROM exams;
На выходе мы получим не только исходный балл, но и новую колонку grade, рассчитанную по правилам.
CASE не меняет данные в таблице сам по себе. Он помогает получить новое значение в результате запроса: категорию, подпись, статус, группу, флаг или расчёт по условию.
Зачем нужен CASE WHEN
Без CASE пришлось бы делать так:
- Забрать из базы сырые данные.
- Передать их в приложение.
- В коде пройтись по строкам.
- Там уже решить, кому поставить
gold, кому silver, а кому bronze.
Это работает, но часто хуже: приложение получает больше данных, логика размазывается между базой и кодом, отчёты становятся менее прозрачными.
CASE позволяет сказать базе:
«Сразу верни данные в том виде, в котором мне нужно их читать».
Например, можно:
- разбить пользователей на группы по возрасту;
- присвоить клиентам уровни по сумме покупок;
- объединить несколько технических статусов в один понятный;
- посчитать разные категории в одном отчёте;
- задать нестандартную сортировку;
- обновить значения по разным правилам.
CASE особенно полезен в отчётах. Он превращает сухие числа и статусы в понятные бизнес-категории.
Базовая форма CASE WHEN
Самая частая форма называется searched CASE.
Она выглядит так:
CASE
WHEN condition_1 THEN result_1
WHEN condition_2 THEN result_2
WHEN condition_3 THEN result_3
ELSE default_result
END
Читается почти как обычный текст:
- если выполнено первое условие — верни первый результат;
- иначе если выполнено второе условие — верни второй результат;
- иначе если выполнено третье условие — верни третий результат;
- если ничего не подошло — верни значение из
ELSE.
Например:
SELECT
student_name,
score,
CASE
WHEN score >= 90 THEN 'excellent'
WHEN score >= 70 THEN 'good'
WHEN score >= 50 THEN 'ok'
ELSE 'failed'
END AS result
FROM exams;
Если у студента score = 82, результат будет good.
Если score = 45, результат будет failed.
CASE проверяет условия сверху вниз
Очень важное правило: условия в CASE проверяются по порядку.
Побеждает первое условие, которое оказалось истинным.
Например:
SELECT
score,
CASE
WHEN score >= 50 THEN 'ok'
WHEN score >= 70 THEN 'good'
WHEN score >= 90 THEN 'excellent'
ELSE 'failed'
END AS result
FROM exams;
На первый взгляд похоже на нормальный запрос, но в нём ошибка логики.
Если score = 95, первое условие score >= 50 уже истинно. Значит SQL вернёт ok и до условий score >= 70 и score >= 90 уже не дойдёт.
Поэтому диапазоны обычно пишут от более строгого условия к более мягкому:
SELECT
score,
CASE
WHEN score >= 90 THEN 'excellent'
WHEN score >= 70 THEN 'good'
WHEN score >= 50 THEN 'ok'
ELSE 'failed'
END AS result
FROM exams;
Теперь всё правильно:
- 95 →
excellent;
- 82 →
good;
- 61 →
ok;
- 40 →
failed.
Пример: уровни клиентов по сумме покупок
Допустим, есть таблица users:
| id |
name |
total_spent |
| 1 |
Anna |
250 |
| 2 |
Bob |
50 |
| 3 |
Vera |
12000 |
| 4 |
Georgy |
1500 |
Хотим присвоить каждому пользователю уровень:
- от 10000 —
platinum;
- от 1000 —
gold;
- от 100 —
silver;
- меньше 100 —
bronze.
Запрос:
SELECT
name,
total_spent,
CASE
WHEN total_spent >= 10000 THEN 'platinum'
WHEN total_spent >= 1000 THEN 'gold'
WHEN total_spent >= 100 THEN 'silver'
ELSE 'bronze'
END AS tier
FROM users;
Результат:
| name |
total_spent |
tier |
| Anna |
250 |
silver |
| Bob |
50 |
bronze |
| Vera |
12000 |
platinum |
| Georgy |
1500 |
gold |
Здесь CASE создаёт новую колонку tier. Её не было в таблице, она появляется только в результате запроса.
Это удобно: сырые данные остаются как есть, а в отчёте мы видим уже понятную классификацию.
ELSE: что вернуть, если ничего не подошло
Часть ELSE отвечает за запасной вариант.
CASE
WHEN total_spent >= 10000 THEN 'platinum'
WHEN total_spent >= 1000 THEN 'gold'
WHEN total_spent >= 100 THEN 'silver'
ELSE 'bronze'
END
Если ни одно условие не сработало, SQL вернёт bronze.
А что будет, если ELSE не написать?
SELECT
name,
total_spent,
CASE
WHEN total_spent >= 10000 THEN 'platinum'
WHEN total_spent >= 1000 THEN 'gold'
WHEN total_spent >= 100 THEN 'silver'
END AS tier
FROM users;
Если у пользователя total_spent = 50, ни одно условие не выполнится. В результате будет NULL.
Это не всегда ошибка. Иногда NULL действительно означает «категория не определена». Но в отчётах такие пустоты часто выглядят странно.
Поэтому хорошая привычка для новичка: почти всегда добавлять ELSE.
Simple CASE: когда сравниваем одну колонку с разными значениями
Есть ещё одна форма CASE. Она называется simple CASE.
Она удобна, когда мы сравниваем одну колонку с конкретными значениями.
Например, есть таблица заказов со статусами:
| id |
status |
| 1 |
pending |
| 2 |
paid |
| 3 |
shipped |
| 4 |
cancelled |
Хотим показать более понятную подпись для каждого статуса:
SELECT
id,
status,
CASE status
WHEN 'pending' THEN 'waiting'
WHEN 'paid' THEN 'paid'
WHEN 'shipped' THEN 'sent'
WHEN 'cancelled' THEN 'cancelled'
ELSE 'unknown'
END AS status_label
FROM orders;
Здесь SQL берёт значение из колонки status и сравнивает его с вариантами в WHEN.
Это короче, чем писать так:
SELECT
id,
status,
CASE
WHEN status = 'pending' THEN 'waiting'
WHEN status = 'paid' THEN 'paid'
WHEN status = 'shipped' THEN 'sent'
WHEN status = 'cancelled' THEN 'cancelled'
ELSE 'unknown'
END AS status_label
FROM orders;
Обе формы нормальные.
Simple CASE хорошо подходит для прямого сравнения одной колонки с набором значений.
Searched CASE нужен, когда условия сложнее: диапазоны, несколько колонок, AND, OR, проверки на NULL.
Когда лучше использовать searched CASE
Searched CASE гибче.
Например, нужно определить тип заказа по сумме и статусу:
SELECT
id,
amount,
status,
CASE
WHEN status = 'cancelled' THEN 'cancelled'
WHEN amount >= 10000 THEN 'large_paid_order'
WHEN amount >= 1000 THEN 'regular_paid_order'
ELSE 'small_paid_order'
END AS order_type
FROM orders
WHERE status IN ('paid', 'cancelled');
Здесь мы проверяем не просто одно значение. У нас есть разные условия: статус, сумма, порядок правил.
Для таких случаев searched CASE читается лучше и даёт больше контроля.
CASE в SELECT
Самое частое место для CASE — список колонок в SELECT.
Например, хотим вывести пользователей и подписать, активны они или нет:
SELECT
id,
email,
CASE
WHEN last_login_at >= CURRENT_DATE - INTERVAL '30 days' THEN 'active'
ELSE 'inactive'
END AS activity_status
FROM users;
В таблице может не быть колонки activity_status. Мы создаём её на лету.
Это удобно для витрин, аналитики, админок и выгрузок: база сразу возвращает не просто даты, суммы и флаги, а понятные категории.
CASE в ORDER BY
CASE можно использовать не только в SELECT.
Например, нужно отсортировать заказы не по алфавиту статуса, а по бизнес-приоритету:
- Сначала оплаченные.
- Потом ожидающие.
- Потом все остальные.
SELECT
id,
status,
created_at
FROM orders
ORDER BY
CASE status
WHEN 'paid' THEN 1
WHEN 'pending' THEN 2
ELSE 3
END,
created_at DESC;
Здесь CASE превращает статус в число для сортировки.
Заказы со статусом paid получают 1 и идут первыми.
Заказы со статусом pending получают 2 и идут после них.
Остальные получают 3.
А внутри каждой группы заказы сортируются по created_at от новых к старым.
Это один из самых красивых сценариев для CASE: мы задаём порядок не по значениям в базе, а по смыслу для бизнеса.
CASE в WHERE
CASE можно использовать и в WHERE, но с ним нужно быть аккуратнее.
Иногда новички пишут слишком сложно:
SELECT
id,
amount
FROM orders
WHERE
CASE
WHEN amount > 1000 THEN TRUE
ELSE FALSE
END;
Такой запрос можно заменить обычным условием:
SELECT
id,
amount
FROM orders
WHERE amount > 1000;
CASE в WHERE нужен только тогда, когда условие действительно ветвится.
Например, логика зависит от роли пользователя:
SELECT
id,
customer_id,
amount
FROM orders
WHERE
CASE
WHEN :role = 'admin' THEN TRUE
WHEN :role = 'user' THEN customer_id = :user_id
ELSE FALSE
END;
Идея такая:
- админ видит все заказы;
- обычный пользователь видит только свои;
- неизвестная роль не видит ничего.
Такой запрос уже действительно использует ветвление.
Но в реальных проектах подобную логику часто стараются писать проще через обычные AND и OR, чтобы оптимизатору базы было легче работать с условиями.
CASE в UPDATE
CASE можно использовать и при обновлении данных.
Например, хотим пересчитать уровень пользователя по сумме покупок и записать его в колонку tier:
UPDATE users
SET tier = CASE
WHEN total_spent >= 10000 THEN 'platinum'
WHEN total_spent >= 1000 THEN 'gold'
WHEN total_spent >= 100 THEN 'silver'
ELSE 'bronze'
END;
Здесь CASE уже не просто показывает значение в результате. Он помогает записать новое значение в таблицу.
Такой подход полезен для массовых обновлений, когда разные строки должны получить разные значения по одному набору правил.
Например, можно закрыть старые заказы по-разному:
UPDATE orders
SET status = CASE
WHEN paid_at IS NOT NULL THEN 'paid'
WHEN cancelled_at IS NOT NULL THEN 'cancelled'
ELSE 'pending'
END
WHERE status IS NULL;
Каждая строка получит свой статус в зависимости от данных внутри этой строки.
Условный подсчёт через SUM и CASE
Один из самых полезных приёмов в аналитике — считать строки по условиям в одном запросе.
Допустим, есть таблица users:
| id |
country |
tier |
| 1 |
USA |
free |
| 2 |
USA |
gold |
| 3 |
USA |
free |
| 4 |
Germany |
gold |
| 5 |
Germany |
free |
Хотим получить по каждой стране:
- всего пользователей;
- сколько пользователей на бесплатном тарифе;
- сколько пользователей на золотом тарифе.
Можно написать так:
SELECT
country,
COUNT(*) AS total_users,
SUM(CASE WHEN tier = 'free' THEN 1 ELSE 0 END) AS free_users,
SUM(CASE WHEN tier = 'gold' THEN 1 ELSE 0 END) AS gold_users
FROM users
GROUP BY country;
Как это работает?
Для каждой строки выражение:
CASE WHEN tier = 'free' THEN 1 ELSE 0 END
возвращает 1, если пользователь на тарифе free, и 0 в остальных случаях.
Потом SUM складывает эти единицы и нули.
Получается счётчик по условию.
Результат:
| country |
total_users |
free_users |
gold_users |
| Germany |
2 |
1 |
1 |
| USA |
3 |
2 |
1 |
Это похоже на маленькую сводную таблицу прямо в SQL.
Более короткий вариант в PostgreSQL: FILTER
В PostgreSQL есть более чистая форма для условного подсчёта — FILTER.
SELECT
country,
COUNT(*) AS total_users,
COUNT(*) FILTER (WHERE tier = 'free') AS free_users,
COUNT(*) FILTER (WHERE tier = 'gold') AS gold_users
FROM users
GROUP BY country;
Логика та же:
COUNT(*) считает всех;
COUNT(*) FILTER (WHERE tier = 'free') считает только пользователей с tier = 'free';
COUNT(*) FILTER (WHERE tier = 'gold') считает только пользователей с tier = 'gold'.
FILTER читается приятнее, но поддерживается не во всех базах данных. Например, в MySQL обычно используют вариант через SUM(CASE WHEN ... THEN 1 ELSE 0 END).
Поэтому CASE всё равно важно знать: он универсальнее и встречается почти везде.
Условные суммы через CASE
Через CASE можно считать не только количество, но и суммы.
Например, хотим посчитать выручку по статусам заказов:
SELECT
SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS paid_amount,
SUM(CASE WHEN status = 'refunded' THEN amount ELSE 0 END) AS refunded_amount,
SUM(CASE WHEN status = 'cancelled' THEN amount ELSE 0 END) AS cancelled_amount
FROM orders;
Здесь каждая сумма берёт только нужные строки.
Для оплаченных заказов:
CASE WHEN status = 'paid' THEN amount ELSE 0 END
Если заказ оплачен — берём его сумму.
Если нет — подставляем 0.
Потом SUM складывает всё в одну итоговую цифру.
Это очень частый приём в финансовых отчётах.
Группировка по CASE
Иногда нужно сгруппировать строки не по готовой колонке, а по категории, которую мы вычисляем сами.
Например, хотим объединить пользователей в две группы:
paid — если тариф gold или platinum;
free — все остальные.
SELECT
CASE
WHEN tier IN ('gold', 'platinum') THEN 'paid'
ELSE 'free'
END AS plan_group,
COUNT(*) AS users_count
FROM users
GROUP BY
CASE
WHEN tier IN ('gold', 'platinum') THEN 'paid'
ELSE 'free'
END;
Результат может быть таким:
| plan_group |
users_count |
| free |
1200 |
| paid |
340 |
Да, выражение CASE пришлось повторить в GROUP BY.
Это обычная ситуация: база должна понимать, по какому именно выражению группировать строки.
Если повтор кажется слишком громоздким, можно вынести расчёт в CTE:
WITH users_with_plan_group AS (
SELECT
id,
CASE
WHEN tier IN ('gold', 'platinum') THEN 'paid'
ELSE 'free'
END AS plan_group
FROM users
)
SELECT
plan_group,
COUNT(*) AS users_count
FROM users_with_plan_group
GROUP BY plan_group;
Так запрос читается спокойнее: сначала мы подготовили категорию, потом сгруппировали по ней.
Типы данных в ветках CASE
Все ветки CASE должны возвращать совместимые типы.
Вот плохой пример:
SELECT
CASE
WHEN amount >= 1000 THEN 'large'
ELSE 0
END AS order_size
FROM orders;
В одной ветке возвращается текст large, в другой — число 0.
База должна привести результат к одному типу. В PostgreSQL такой запрос может закончиться ошибкой, потому что непонятно, как честно совместить текст и число.
Лучше сделать все ветки текстовыми:
SELECT
CASE
WHEN amount >= 1000 THEN 'large'
ELSE 'small'
END AS order_size
FROM orders;
Или все ветки числовыми:
SELECT
CASE
WHEN amount >= 1000 THEN 1
ELSE 0
END AS is_large_order
FROM orders;
Главное правило: один CASE — один понятный тип результата.
Если это текстовая категория, пусть все ветки возвращают текст.
Если это числовой флаг, пусть все ветки возвращают числа.
Если это дата, пусть все ветки возвращают даты.
CASE и NULL
С NULL в SQL всегда нужно быть внимательным.
Ошибка новичка:
SELECT
id,
CASE
WHEN phone = NULL THEN 'no_phone'
ELSE 'has_phone'
END AS phone_status
FROM users;
Так писать нельзя.
NULL не сравнивается через =. В SQL NULL означает «значение отсутствует» или «значение неизвестно». Проверять его нужно через IS NULL.
Правильно:
SELECT
id,
CASE
WHEN phone IS NULL THEN 'no_phone'
ELSE 'has_phone'
END AS phone_status
FROM users;
Для обратной проверки используют IS NOT NULL:
SELECT
id,
CASE
WHEN phone IS NOT NULL THEN 'has_phone'
ELSE 'no_phone'
END AS phone_status
FROM users;
Это один из тех моментов, которые лучше запомнить сразу: в SQL почти никогда не нужно писать = NULL.
Пример: аккуратный отчёт по заказам
Допустим, есть таблица orders:
| id |
customer_id |
status |
amount |
| 1 |
10 |
paid |
1200 |
| 2 |
10 |
pending |
800 |
| 3 |
15 |
cancelled |
500 |
| 4 |
21 |
paid |
7000 |
| 5 |
21 |
refunded |
7000 |
Хотим получить отчёт, где:
- технические статусы сгруппированы в понятные категории;
- сумма заказа превращается в размер заказа;
- статусы сортируются в нужном порядке.
SELECT
id,
customer_id,
amount,
CASE
WHEN status IN ('paid', 'refunded') THEN 'closed'
WHEN status = 'pending' THEN 'open'
WHEN status = 'cancelled' THEN 'cancelled'
ELSE 'unknown'
END AS status_group,
CASE
WHEN amount >= 5000 THEN 'large'
WHEN amount >= 1000 THEN 'medium'
ELSE 'small'
END AS amount_group
FROM orders
ORDER BY
CASE
WHEN status = 'pending' THEN 1
WHEN status = 'paid' THEN 2
WHEN status = 'refunded' THEN 3
WHEN status = 'cancelled' THEN 4
ELSE 5
END,
id;
Такой запрос уже похож на реальный отчёт.
Мы не просто выводим сырые значения из базы. Мы сразу объясняем их:
status_group показывает смысл статуса;
amount_group показывает размер заказа;
ORDER BY CASE задаёт понятный порядок строк.
Частые ошибки новичков
Забыть END
CASE всегда должен заканчиваться словом END.
Неправильно:
SELECT
CASE
WHEN score >= 90 THEN 'excellent'
ELSE 'ok'
AS result
FROM exams;
Правильно:
SELECT
CASE
WHEN score >= 90 THEN 'excellent'
ELSE 'ok'
END AS result
FROM exams;
END закрывает всю конструкцию CASE.
Поставить условия в неправильном порядке
Неправильно:
SELECT
score,
CASE
WHEN score >= 50 THEN 'ok'
WHEN score >= 70 THEN 'good'
WHEN score >= 90 THEN 'excellent'
ELSE 'failed'
END AS result
FROM exams;
Для score = 95 результат будет ok, потому что первое условие уже сработало.
Правильно:
SELECT
score,
CASE
WHEN score >= 90 THEN 'excellent'
WHEN score >= 70 THEN 'good'
WHEN score >= 50 THEN 'ok'
ELSE 'failed'
END AS result
FROM exams;
Чем строже условие, тем выше оно обычно стоит.
Вернуть разные типы в разных ветках
Неправильно:
SELECT
CASE
WHEN amount > 1000 THEN 'large'
ELSE 0
END AS size
FROM orders;
Лучше:
SELECT
CASE
WHEN amount > 1000 THEN 'large'
ELSE 'small'
END AS size
FROM orders;
Или:
SELECT
CASE
WHEN amount > 1000 THEN 1
ELSE 0
END AS is_large
FROM orders;
Все ветки должны возвращать совместимые значения.
Не писать ELSE и удивляться NULL
Если ELSE нет и ни одно условие не подошло, результат будет NULL.
SELECT
amount,
CASE
WHEN amount >= 1000 THEN 'large'
END AS size
FROM orders;
Для заказа на 500 результатом будет NULL.
Обычно лучше добавить запасной вариант:
SELECT
amount,
CASE
WHEN amount >= 1000 THEN 'large'
ELSE 'small'
END AS size
FROM orders;
Сравнивать NULL через =
Неправильно:
SELECT
CASE
WHEN phone = NULL THEN 'no_phone'
ELSE 'has_phone'
END AS phone_status
FROM users;
Правильно:
SELECT
CASE
WHEN phone IS NULL THEN 'no_phone'
ELSE 'has_phone'
END AS phone_status
FROM users;
Для NULL нужны IS NULL и IS NOT NULL.
Использовать CASE, когда достаточно обычного условия
Слишком сложно:
SELECT
id,
amount
FROM orders
WHERE
CASE
WHEN amount > 1000 THEN TRUE
ELSE FALSE
END;
Проще:
SELECT
id,
amount
FROM orders
WHERE amount > 1000;
CASE нужен для выбора одного результата из нескольких вариантов. Если условие одно и простое, обычный WHERE читается лучше.
Короткая шпаргалка по CASE
Категория по диапазону:
SELECT
amount,
CASE
WHEN amount >= 5000 THEN 'large'
WHEN amount >= 1000 THEN 'medium'
ELSE 'small'
END AS amount_group
FROM orders;
Подпись по статусу:
SELECT
status,
CASE status
WHEN 'pending' THEN 'waiting'
WHEN 'paid' THEN 'paid'
WHEN 'cancelled' THEN 'cancelled'
ELSE 'unknown'
END AS status_label
FROM orders;
Условный подсчёт:
SELECT
country,
SUM(CASE WHEN tier = 'free' THEN 1 ELSE 0 END) AS free_users,
SUM(CASE WHEN tier = 'gold' THEN 1 ELSE 0 END) AS gold_users
FROM users
GROUP BY country;
Условная сортировка:
SELECT
id,
status,
created_at
FROM orders
ORDER BY
CASE status
WHEN 'paid' THEN 1
WHEN 'pending' THEN 2
ELSE 3
END,
created_at DESC;
Проверка на NULL:
SELECT
id,
CASE
WHEN phone IS NULL THEN 'no_phone'
ELSE 'has_phone'
END AS phone_status
FROM users;
Главное из статьи
CASE WHEN — это условная логика внутри SQL-запроса. Он позволяет вернуть разные значения в зависимости от данных в строке.
Самая частая форма:
CASE
WHEN condition THEN result
ELSE default_result
END
CASE проверяет условия сверху вниз и возвращает результат первого истинного условия.
Если ELSE не указан и ни одно условие не подошло, результатом будет NULL.
Есть две формы:
- searched
CASE — для условий, диапазонов и сложной логики;
- simple
CASE — для сравнения одной колонки с набором значений.
CASE можно использовать в SELECT, ORDER BY, WHERE, UPDATE, агрегатах и группировках.
Для условного подсчёта часто используют связку SUM(CASE WHEN ... THEN 1 ELSE 0 END). В PostgreSQL для этого также есть удобный синтаксис FILTER.
Все ветки CASE должны возвращать совместимые типы: текст с текстом, числа с числами, даты с датами.
Для проверки NULL нужно писать IS NULL или IS NOT NULL, а не = NULL.
Главная польза CASE в том, что он позволяет превратить сырые данные в понятные категории прямо внутри SQL. Благодаря этому запросы становятся не просто способом достать строки из таблицы, а инструментом для аккуратных отчётов, бизнес-логики и понятной аналитики.
CASE WHEN— это способ написать условие внутри SQL-запроса.Если совсем просто, это SQL-версия привычного
if/else.В обычном коде мы можем написать: если оценка больше или равна 90 — поставить
A, если больше или равна 70 — поставитьB, иначе —F.В SQL такая логика записывается через
CASE:SELECT student_name, score, CASE WHEN score >= 90 THEN 'A' WHEN score >= 70 THEN 'B' WHEN score >= 50 THEN 'C' ELSE 'F' END AS grade FROM exams;На выходе мы получим не только исходный балл, но и новую колонку
grade, рассчитанную по правилам.CASEне меняет данные в таблице сам по себе. Он помогает получить новое значение в результате запроса: категорию, подпись, статус, группу, флаг или расчёт по условию.Зачем нужен
CASE WHENБез
CASEпришлось бы делать так:gold, комуsilver, а комуbronze.Это работает, но часто хуже: приложение получает больше данных, логика размазывается между базой и кодом, отчёты становятся менее прозрачными.
CASEпозволяет сказать базе:«Сразу верни данные в том виде, в котором мне нужно их читать».
Например, можно:
CASEособенно полезен в отчётах. Он превращает сухие числа и статусы в понятные бизнес-категории.Базовая форма
CASE WHENСамая частая форма называется searched
CASE.Она выглядит так:
CASE WHEN condition_1 THEN result_1 WHEN condition_2 THEN result_2 WHEN condition_3 THEN result_3 ELSE default_result ENDЧитается почти как обычный текст:
ELSE.Например:
SELECT student_name, score, CASE WHEN score >= 90 THEN 'excellent' WHEN score >= 70 THEN 'good' WHEN score >= 50 THEN 'ok' ELSE 'failed' END AS result FROM exams;Если у студента
score = 82, результат будетgood.Если
score = 45, результат будетfailed.CASEпроверяет условия сверху внизОчень важное правило: условия в
CASEпроверяются по порядку.Побеждает первое условие, которое оказалось истинным.
Например:
SELECT score, CASE WHEN score >= 50 THEN 'ok' WHEN score >= 70 THEN 'good' WHEN score >= 90 THEN 'excellent' ELSE 'failed' END AS result FROM exams;На первый взгляд похоже на нормальный запрос, но в нём ошибка логики.
Если
score = 95, первое условиеscore >= 50уже истинно. Значит SQL вернётokи до условийscore >= 70иscore >= 90уже не дойдёт.Поэтому диапазоны обычно пишут от более строгого условия к более мягкому:
SELECT score, CASE WHEN score >= 90 THEN 'excellent' WHEN score >= 70 THEN 'good' WHEN score >= 50 THEN 'ok' ELSE 'failed' END AS result FROM exams;Теперь всё правильно:
excellent;good;ok;failed.Пример: уровни клиентов по сумме покупок
Допустим, есть таблица
users:Хотим присвоить каждому пользователю уровень:
platinum;gold;silver;bronze.Запрос:
SELECT name, total_spent, CASE WHEN total_spent >= 10000 THEN 'platinum' WHEN total_spent >= 1000 THEN 'gold' WHEN total_spent >= 100 THEN 'silver' ELSE 'bronze' END AS tier FROM users;Результат:
Здесь
CASEсоздаёт новую колонкуtier. Её не было в таблице, она появляется только в результате запроса.Это удобно: сырые данные остаются как есть, а в отчёте мы видим уже понятную классификацию.
ELSE: что вернуть, если ничего не подошлоЧасть
ELSEотвечает за запасной вариант.CASE WHEN total_spent >= 10000 THEN 'platinum' WHEN total_spent >= 1000 THEN 'gold' WHEN total_spent >= 100 THEN 'silver' ELSE 'bronze' ENDЕсли ни одно условие не сработало, SQL вернёт
bronze.А что будет, если
ELSEне написать?SELECT name, total_spent, CASE WHEN total_spent >= 10000 THEN 'platinum' WHEN total_spent >= 1000 THEN 'gold' WHEN total_spent >= 100 THEN 'silver' END AS tier FROM users;Если у пользователя
total_spent = 50, ни одно условие не выполнится. В результате будетNULL.Это не всегда ошибка. Иногда
NULLдействительно означает «категория не определена». Но в отчётах такие пустоты часто выглядят странно.Поэтому хорошая привычка для новичка: почти всегда добавлять
ELSE.Simple
CASE: когда сравниваем одну колонку с разными значениямиЕсть ещё одна форма
CASE. Она называется simpleCASE.Она удобна, когда мы сравниваем одну колонку с конкретными значениями.
Например, есть таблица заказов со статусами:
Хотим показать более понятную подпись для каждого статуса:
SELECT id, status, CASE status WHEN 'pending' THEN 'waiting' WHEN 'paid' THEN 'paid' WHEN 'shipped' THEN 'sent' WHEN 'cancelled' THEN 'cancelled' ELSE 'unknown' END AS status_label FROM orders;Здесь SQL берёт значение из колонки
statusи сравнивает его с вариантами вWHEN.Это короче, чем писать так:
SELECT id, status, CASE WHEN status = 'pending' THEN 'waiting' WHEN status = 'paid' THEN 'paid' WHEN status = 'shipped' THEN 'sent' WHEN status = 'cancelled' THEN 'cancelled' ELSE 'unknown' END AS status_label FROM orders;Обе формы нормальные.
Simple
CASEхорошо подходит для прямого сравнения одной колонки с набором значений.Searched
CASEнужен, когда условия сложнее: диапазоны, несколько колонок,AND,OR, проверки наNULL.Когда лучше использовать searched
CASESearched
CASEгибче.Например, нужно определить тип заказа по сумме и статусу:
SELECT id, amount, status, CASE WHEN status = 'cancelled' THEN 'cancelled' WHEN amount >= 10000 THEN 'large_paid_order' WHEN amount >= 1000 THEN 'regular_paid_order' ELSE 'small_paid_order' END AS order_type FROM orders WHERE status IN ('paid', 'cancelled');Здесь мы проверяем не просто одно значение. У нас есть разные условия: статус, сумма, порядок правил.
Для таких случаев searched
CASEчитается лучше и даёт больше контроля.CASEвSELECTСамое частое место для
CASE— список колонок вSELECT.Например, хотим вывести пользователей и подписать, активны они или нет:
SELECT id, email, CASE WHEN last_login_at >= CURRENT_DATE - INTERVAL '30 days' THEN 'active' ELSE 'inactive' END AS activity_status FROM users;В таблице может не быть колонки
activity_status. Мы создаём её на лету.Это удобно для витрин, аналитики, админок и выгрузок: база сразу возвращает не просто даты, суммы и флаги, а понятные категории.
CASEвORDER BYCASEможно использовать не только вSELECT.Например, нужно отсортировать заказы не по алфавиту статуса, а по бизнес-приоритету:
SELECT id, status, created_at FROM orders ORDER BY CASE status WHEN 'paid' THEN 1 WHEN 'pending' THEN 2 ELSE 3 END, created_at DESC;Здесь
CASEпревращает статус в число для сортировки.Заказы со статусом
paidполучают 1 и идут первыми.Заказы со статусом
pendingполучают 2 и идут после них.Остальные получают 3.
А внутри каждой группы заказы сортируются по
created_atот новых к старым.Это один из самых красивых сценариев для
CASE: мы задаём порядок не по значениям в базе, а по смыслу для бизнеса.CASEвWHERECASEможно использовать и вWHERE, но с ним нужно быть аккуратнее.Иногда новички пишут слишком сложно:
SELECT id, amount FROM orders WHERE CASE WHEN amount > 1000 THEN TRUE ELSE FALSE END;Такой запрос можно заменить обычным условием:
SELECT id, amount FROM orders WHERE amount > 1000;CASEвWHEREнужен только тогда, когда условие действительно ветвится.Например, логика зависит от роли пользователя:
SELECT id, customer_id, amount FROM orders WHERE CASE WHEN :role = 'admin' THEN TRUE WHEN :role = 'user' THEN customer_id = :user_id ELSE FALSE END;Идея такая:
Такой запрос уже действительно использует ветвление.
Но в реальных проектах подобную логику часто стараются писать проще через обычные
ANDиOR, чтобы оптимизатору базы было легче работать с условиями.CASEвUPDATECASEможно использовать и при обновлении данных.Например, хотим пересчитать уровень пользователя по сумме покупок и записать его в колонку
tier:UPDATE users SET tier = CASE WHEN total_spent >= 10000 THEN 'platinum' WHEN total_spent >= 1000 THEN 'gold' WHEN total_spent >= 100 THEN 'silver' ELSE 'bronze' END;Здесь
CASEуже не просто показывает значение в результате. Он помогает записать новое значение в таблицу.Такой подход полезен для массовых обновлений, когда разные строки должны получить разные значения по одному набору правил.
Например, можно закрыть старые заказы по-разному:
UPDATE orders SET status = CASE WHEN paid_at IS NOT NULL THEN 'paid' WHEN cancelled_at IS NOT NULL THEN 'cancelled' ELSE 'pending' END WHERE status IS NULL;Каждая строка получит свой статус в зависимости от данных внутри этой строки.
Условный подсчёт через
SUMиCASEОдин из самых полезных приёмов в аналитике — считать строки по условиям в одном запросе.
Допустим, есть таблица
users:Хотим получить по каждой стране:
Можно написать так:
SELECT country, COUNT(*) AS total_users, SUM(CASE WHEN tier = 'free' THEN 1 ELSE 0 END) AS free_users, SUM(CASE WHEN tier = 'gold' THEN 1 ELSE 0 END) AS gold_users FROM users GROUP BY country;Как это работает?
Для каждой строки выражение:
CASE WHEN tier = 'free' THEN 1 ELSE 0 ENDвозвращает 1, если пользователь на тарифе
free, и 0 в остальных случаях.Потом
SUMскладывает эти единицы и нули.Получается счётчик по условию.
Результат:
Это похоже на маленькую сводную таблицу прямо в SQL.
Более короткий вариант в PostgreSQL:
FILTERВ PostgreSQL есть более чистая форма для условного подсчёта —
FILTER.SELECT country, COUNT(*) AS total_users, COUNT(*) FILTER (WHERE tier = 'free') AS free_users, COUNT(*) FILTER (WHERE tier = 'gold') AS gold_users FROM users GROUP BY country;Логика та же:
COUNT(*)считает всех;COUNT(*) FILTER (WHERE tier = 'free')считает только пользователей сtier = 'free';COUNT(*) FILTER (WHERE tier = 'gold')считает только пользователей сtier = 'gold'.FILTERчитается приятнее, но поддерживается не во всех базах данных. Например, в MySQL обычно используют вариант черезSUM(CASE WHEN ... THEN 1 ELSE 0 END).Поэтому
CASEвсё равно важно знать: он универсальнее и встречается почти везде.Условные суммы через
CASEЧерез
CASEможно считать не только количество, но и суммы.Например, хотим посчитать выручку по статусам заказов:
SELECT SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS paid_amount, SUM(CASE WHEN status = 'refunded' THEN amount ELSE 0 END) AS refunded_amount, SUM(CASE WHEN status = 'cancelled' THEN amount ELSE 0 END) AS cancelled_amount FROM orders;Здесь каждая сумма берёт только нужные строки.
Для оплаченных заказов:
CASE WHEN status = 'paid' THEN amount ELSE 0 ENDЕсли заказ оплачен — берём его сумму.
Если нет — подставляем 0.
Потом
SUMскладывает всё в одну итоговую цифру.Это очень частый приём в финансовых отчётах.
Группировка по
CASEИногда нужно сгруппировать строки не по готовой колонке, а по категории, которую мы вычисляем сами.
Например, хотим объединить пользователей в две группы:
paid— если тарифgoldилиplatinum;free— все остальные.SELECT CASE WHEN tier IN ('gold', 'platinum') THEN 'paid' ELSE 'free' END AS plan_group, COUNT(*) AS users_count FROM users GROUP BY CASE WHEN tier IN ('gold', 'platinum') THEN 'paid' ELSE 'free' END;Результат может быть таким:
Да, выражение
CASEпришлось повторить вGROUP BY.Это обычная ситуация: база должна понимать, по какому именно выражению группировать строки.
Если повтор кажется слишком громоздким, можно вынести расчёт в CTE:
WITH users_with_plan_group AS ( SELECT id, CASE WHEN tier IN ('gold', 'platinum') THEN 'paid' ELSE 'free' END AS plan_group FROM users ) SELECT plan_group, COUNT(*) AS users_count FROM users_with_plan_group GROUP BY plan_group;Так запрос читается спокойнее: сначала мы подготовили категорию, потом сгруппировали по ней.
Типы данных в ветках
CASEВсе ветки
CASEдолжны возвращать совместимые типы.Вот плохой пример:
SELECT CASE WHEN amount >= 1000 THEN 'large' ELSE 0 END AS order_size FROM orders;В одной ветке возвращается текст
large, в другой — число 0.База должна привести результат к одному типу. В PostgreSQL такой запрос может закончиться ошибкой, потому что непонятно, как честно совместить текст и число.
Лучше сделать все ветки текстовыми:
SELECT CASE WHEN amount >= 1000 THEN 'large' ELSE 'small' END AS order_size FROM orders;Или все ветки числовыми:
SELECT CASE WHEN amount >= 1000 THEN 1 ELSE 0 END AS is_large_order FROM orders;Главное правило: один
CASE— один понятный тип результата.Если это текстовая категория, пусть все ветки возвращают текст.
Если это числовой флаг, пусть все ветки возвращают числа.
Если это дата, пусть все ветки возвращают даты.
CASEиNULLС
NULLв SQL всегда нужно быть внимательным.Ошибка новичка:
SELECT id, CASE WHEN phone = NULL THEN 'no_phone' ELSE 'has_phone' END AS phone_status FROM users;Так писать нельзя.
NULLне сравнивается через=. В SQLNULLозначает «значение отсутствует» или «значение неизвестно». Проверять его нужно черезIS NULL.Правильно:
SELECT id, CASE WHEN phone IS NULL THEN 'no_phone' ELSE 'has_phone' END AS phone_status FROM users;Для обратной проверки используют
IS NOT NULL:SELECT id, CASE WHEN phone IS NOT NULL THEN 'has_phone' ELSE 'no_phone' END AS phone_status FROM users;Это один из тех моментов, которые лучше запомнить сразу: в SQL почти никогда не нужно писать
= NULL.Пример: аккуратный отчёт по заказам
Допустим, есть таблица
orders:Хотим получить отчёт, где:
SELECT id, customer_id, amount, CASE WHEN status IN ('paid', 'refunded') THEN 'closed' WHEN status = 'pending' THEN 'open' WHEN status = 'cancelled' THEN 'cancelled' ELSE 'unknown' END AS status_group, CASE WHEN amount >= 5000 THEN 'large' WHEN amount >= 1000 THEN 'medium' ELSE 'small' END AS amount_group FROM orders ORDER BY CASE WHEN status = 'pending' THEN 1 WHEN status = 'paid' THEN 2 WHEN status = 'refunded' THEN 3 WHEN status = 'cancelled' THEN 4 ELSE 5 END, id;Такой запрос уже похож на реальный отчёт.
Мы не просто выводим сырые значения из базы. Мы сразу объясняем их:
status_groupпоказывает смысл статуса;amount_groupпоказывает размер заказа;ORDER BY CASEзадаёт понятный порядок строк.Частые ошибки новичков
Забыть
ENDCASEвсегда должен заканчиваться словомEND.Неправильно:
SELECT CASE WHEN score >= 90 THEN 'excellent' ELSE 'ok' AS result FROM exams;Правильно:
SELECT CASE WHEN score >= 90 THEN 'excellent' ELSE 'ok' END AS result FROM exams;ENDзакрывает всю конструкциюCASE.Поставить условия в неправильном порядке
Неправильно:
SELECT score, CASE WHEN score >= 50 THEN 'ok' WHEN score >= 70 THEN 'good' WHEN score >= 90 THEN 'excellent' ELSE 'failed' END AS result FROM exams;Для
score = 95результат будетok, потому что первое условие уже сработало.Правильно:
SELECT score, CASE WHEN score >= 90 THEN 'excellent' WHEN score >= 70 THEN 'good' WHEN score >= 50 THEN 'ok' ELSE 'failed' END AS result FROM exams;Чем строже условие, тем выше оно обычно стоит.
Вернуть разные типы в разных ветках
Неправильно:
SELECT CASE WHEN amount > 1000 THEN 'large' ELSE 0 END AS size FROM orders;Лучше:
SELECT CASE WHEN amount > 1000 THEN 'large' ELSE 'small' END AS size FROM orders;Или:
SELECT CASE WHEN amount > 1000 THEN 1 ELSE 0 END AS is_large FROM orders;Все ветки должны возвращать совместимые значения.
Не писать
ELSEи удивлятьсяNULLЕсли
ELSEнет и ни одно условие не подошло, результат будетNULL.SELECT amount, CASE WHEN amount >= 1000 THEN 'large' END AS size FROM orders;Для заказа на 500 результатом будет
NULL.Обычно лучше добавить запасной вариант:
SELECT amount, CASE WHEN amount >= 1000 THEN 'large' ELSE 'small' END AS size FROM orders;Сравнивать
NULLчерез=Неправильно:
SELECT CASE WHEN phone = NULL THEN 'no_phone' ELSE 'has_phone' END AS phone_status FROM users;Правильно:
SELECT CASE WHEN phone IS NULL THEN 'no_phone' ELSE 'has_phone' END AS phone_status FROM users;Для
NULLнужныIS NULLиIS NOT NULL.Использовать
CASE, когда достаточно обычного условияСлишком сложно:
SELECT id, amount FROM orders WHERE CASE WHEN amount > 1000 THEN TRUE ELSE FALSE END;Проще:
SELECT id, amount FROM orders WHERE amount > 1000;CASEнужен для выбора одного результата из нескольких вариантов. Если условие одно и простое, обычныйWHEREчитается лучше.Короткая шпаргалка по
CASEКатегория по диапазону:
SELECT amount, CASE WHEN amount >= 5000 THEN 'large' WHEN amount >= 1000 THEN 'medium' ELSE 'small' END AS amount_group FROM orders;Подпись по статусу:
SELECT status, CASE status WHEN 'pending' THEN 'waiting' WHEN 'paid' THEN 'paid' WHEN 'cancelled' THEN 'cancelled' ELSE 'unknown' END AS status_label FROM orders;Условный подсчёт:
SELECT country, SUM(CASE WHEN tier = 'free' THEN 1 ELSE 0 END) AS free_users, SUM(CASE WHEN tier = 'gold' THEN 1 ELSE 0 END) AS gold_users FROM users GROUP BY country;Условная сортировка:
SELECT id, status, created_at FROM orders ORDER BY CASE status WHEN 'paid' THEN 1 WHEN 'pending' THEN 2 ELSE 3 END, created_at DESC;Проверка на
NULL:SELECT id, CASE WHEN phone IS NULL THEN 'no_phone' ELSE 'has_phone' END AS phone_status FROM users;Главное из статьи
CASE WHEN— это условная логика внутри SQL-запроса. Он позволяет вернуть разные значения в зависимости от данных в строке.Самая частая форма:
CASE WHEN condition THEN result ELSE default_result ENDCASEпроверяет условия сверху вниз и возвращает результат первого истинного условия.Если
ELSEне указан и ни одно условие не подошло, результатом будетNULL.Есть две формы:
CASE— для условий, диапазонов и сложной логики;CASE— для сравнения одной колонки с набором значений.CASEможно использовать вSELECT,ORDER BY,WHERE,UPDATE, агрегатах и группировках.Для условного подсчёта часто используют связку
SUM(CASE WHEN ... THEN 1 ELSE 0 END). В PostgreSQL для этого также есть удобный синтаксисFILTER.Все ветки
CASEдолжны возвращать совместимые типы: текст с текстом, числа с числами, даты с датами.Для проверки
NULLнужно писатьIS NULLилиIS NOT NULL, а не= NULL.Главная польза
CASEв том, что он позволяет превратить сырые данные в понятные категории прямо внутри SQL. Благодаря этому запросы становятся не просто способом достать строки из таблицы, а инструментом для аккуратных отчётов, бизнес-логики и понятной аналитики.