SQLCASEtutorial

CASE WHEN в SQL: как писать условия прямо внутри запроса

CASE WHEN — это «if/else внутри SQL». Простыми словами: как поставить условие прямо в SELECT, разница между searched CASE и simple CASE, бакетирование чисел в категории, pivot одной формулой и условные агрегаты. С таблицами и частыми ошибками.

11 мин чтенияСправочникSQL · CASE · tutorial

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 пришлось бы делать так:

  1. Забрать из базы сырые данные.
  2. Передать их в приложение.
  3. В коде пройтись по строкам.
  4. Там уже решить, кому поставить 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.

Например, нужно отсортировать заказы не по алфавиту статуса, а по бизнес-приоритету:

  1. Сначала оплаченные.
  2. Потом ожидающие.
  3. Потом все остальные.
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. Благодаря этому запросы становятся не просто способом достать строки из таблицы, а инструментом для аккуратных отчётов, бизнес-логики и понятной аналитики.

Закрепи на практике

Решай задачи в SQL-тренажёре с мгновенной проверкой и подсказками.

Открыть тренажёр