11. CASE выражения
«В императивном коде ветвление направляет поток управления от одной инструкции к другой. В декларативном реляционном запросе ветвление преобразует значение выражения, ни на йоту не нарушая детерминированный поток кортежей».
Ветвление в декларативном мире: Условная логика в SQL
В парадигме реляционных баз данных мы оперируем строгими математическими множествами. Мы привыкли считать, что алгоритмическое ветвление (if-else, switch-case) — это исключительная прерогатива прикладных языков общего назначения, таких как Go или C++.
Однако при проектировании высоконагруженных серверных систем регулярно возникает архитектурная необходимость трансформировать данные «на лету» непосредственно на стороне СУБД. Вытягивать гигабайты сырых кортежей по сети, чтобы затем в памяти Go раскладывать их по условиям, сжигая пропускную способность сокетов и нагружая Garbage Collector — непозволительное расточительство.
Для реализации выразительной условной логики стандарт ANSI SQL предоставляет мощную конструкцию CASE.
С архитектурной точки зрения CASE — это не оператор потока управления, а скалярное выражение, которое можно органично встраивать в этапы проекции (SELECT), фильтрации (WHERE), группировки (GROUP BY), сортировки (ORDER BY) или обновления данных (UPDATE).
Два формата CASE
Стандарт реляционного языка определяет выражение CASE в двух синтаксических вариациях:
1. Simple CASE (Простой CASE)
Синтаксически и семантически простая форма аналогична конструкции switch x в Go. Она сопоставляет результат одного базового выражения со списком фиксированных значений. Это идеальный инструмент для маппинга перечислений (enums) и кодов статусов:
SELECT
id,
status,
CASE status
WHEN 'pending' THEN 'Ожидает'
WHEN 'paid' THEN 'Оплачен'
WHEN 'cancelled' THEN 'Отменен'
ELSE 'Неизвестно'
END AS status_ru
FROM orders;
Движок базы данных единожды вычисляет значение атрибута status и последовательно сравнивает его на равенство с константами в секциях WHEN.
2. Searched CASE (Поисковый CASE)
Поисковая форма представляет собой полноценный эквивалент каскадного ветвления if - else if - else. В каждом блоке WHEN задается произвольный независимый логический предикат любой степени сложности:
SELECT
id,
amount,
CASE
WHEN amount > 100000 THEN 'VIP'
WHEN amount > 50000 AND status = 'paid' THEN 'Premium'
ELSE 'Regular'
END AS customer_segment
FROM orders;
Под капотом: Ленивое вычисление (Short-circuit)
Как именно процессор базы данных физически исполняет вычисление конструкции CASE для миллионов кортежей?
В точности как логические операторы && и || в рантайме Go, выражение CASE реализует детерминированное короткое замыкание (Short-circuit evaluation).
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
Row["Строка из Buffer Pool"]:::warning --> CaseStart["Начало CASE"]
CaseStart --> Cond1{"Условие 1<br>WHEN amount > 1000"}
Cond1 -->|"True"| Res1["Вычисление результата 1"]
Cond1 -->|"False"| Cond2{"Условие 2<br>WHEN amount > 500"}
Cond2 -->|"True"| Res2["Вычисление результата 2"]
Cond2 -->|"False"| CondElse["Блок ELSE"]
CondElse --> Res3["Вычисление ELSE"]
Res1 --> EndCase["Возврат значения<br>в этап Projection"]
Res2 --> EndCase
Res3 --> EndCase
Исполнитель СУБД валидирует условия строго сверху вниз в порядке их объявления. Как только очередная ветка WHEN возвращает TRUE:
- Процессор немедленно вычисляет выражение соответствующего блока
THEN; - Все последующие ветки
WHENи секцияELSEполностью игнорируются и физически не исполняются; - Результат мгновенно передается на следующий этап конвейера.
Паттерн: Условная агрегация (Conditional Aggregation / Pivot)
Это главный промышленный паттерн применения CASE, тестирование которого встречается на 9 из 10 технических собеседований на позицию Senior Backend Engineer.
Практическая задача: Сформировать финансовый отчет, отобразив в одной-единственной результирующей строке три метрики: совокупный оборот всех заказов, сумму только оплаченных покупок и сумму только отмененных заказов.
❌ Решение на стороне Go (Архитектурный антипаттерн):
Разработчик отправляет запрос SELECT amount, status FROM orders, вытягивает 5 000 000 строк по TCP в оперативную память Go-сервиса, аллоцирует огромный слайс структур и раскладывает суммы тремя ветвлениями if внутри цикла for.
Последствия: Сетевой канал перегружен (Network IO), рантайм Go захлебывается в аллокациях памяти, а сборщик мусора уходит в затяжную паузу Mark-and-Sweep.
✅ Высокопроизводительное решение на стороне СУБД (Условная агрегация):
Мы объединяем агрегатную функцию SUM из статьи 7. GROUP BY и агрегатные функции с выражением CASE:
SELECT
SUM(amount) AS total_all,
SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS total_paid,
SUM(CASE WHEN status = 'cancelled' THEN amount ELSE 0 END) AS total_cancelled
FROM orders;
Mechanical Sympathy: База данных последовательно читает таблицу ровно один раз (Sequential Scan). Выражение CASE вычисляется прямо в регистрах CPU сервера СУБД при обработке кортежа в Buffer Pool. В сетевой сокет Go-приложения отправляется ровно одна компактная строка из трех чисел (24 байта). Нагрузка на сеть и аллокатор Go равна абсолютному нулю!
Паттерн: Кастомная сортировка в ORDER BY
В реальных бизнес-системах порядок выдачи сущностей часто диктуется сложной логикой приоритетов, не укладывающейся в стандартную сортировку по алфавиту или ID.
Например, в трекере инцидентов тикеты со статусом CRITICAL должны идти первыми, за ними — HIGH, затем — MEDIUM, а остальные упорядочиваться по дате создания.
SELECT id, title, severity, created_at
FROM tickets
ORDER BY
CASE severity
WHEN 'CRITICAL' THEN 1
WHEN 'HIGH' THEN 2
WHEN 'MEDIUM' THEN 3
ELSE 4
END,
created_at DESC;
Архитектурный холивар: SQL CASE vs Go Switch
Где на самом деле должна располагаться условная логика: в базе данных или на уровне бэкенд-сервисов?
Зрелый системный инженер всегда оценивает компромисс между дефицитным CPU базы данных и пропускной способностью сети:
-
Используйте SQL CASE, если:
- Условие критично для редукции данных и агрегации (паттерн
SUM(CASE...)), что кардинально снижает сетевой трафик. - Условие участвует в фильтрации
WHEREили кастомной сортировкеORDER BYна стороне СУБД. -
Выполняется атомарное пакетное обновление строк с разными состояниями:
SQL UPDATE users SET tier = CASE WHEN points > 1000 THEN 'Gold' ELSE 'Silver' END;
- Условие критично для редукции данных и агрегации (паттерн
-
Используйте Go Switch, если:
- Ветвление относится к уровню представления (Presentation / Transport Layer). Например, локализация кода статуса
order_status_createdв читаемую строку"Заказ оформлен"для вывода в JSON. - Процессор базы данных является узким местом (bottleneck). Манипуляции со строками, парсинг регулярных выражений и форматирование дат внутри
CASEсжигают процессорные такты сервера СУБД. Процессор базы данных масштабировать невероятно дорого. Напротив, инстансы Go-микросервисов в Kubernetes масштабируются горизонтально за секунды. Держите базу свободной для хранения и выборки, а бизнес-маппинг выполняйте в Go.
- Ветвление относится к уровню представления (Presentation / Transport Layer). Например, локализация кода статуса
Итог
CASE— единственный стандартный механизм декларативного ветвления логики непосредственно внутри SQL-запроса.CASEвычисляется по принципу короткого замыкания (short-circuit) сверху вниз, защищая от фатальных ошибок времени выполнения вроде деления на ноль.- Условная агрегация (
SUM(CASE...)) — мощный инструмент для построения сводных отчетов (Pivot) за один последовательный проход таблицы без сетевого оверхеда. - Размещение
CASEв секцииORDER BYдает гибкость кастомного ранжирования, но отключает использование B-Tree индексов для сортировки. - Опущенная ветвь
ELSEнеявно возвращаетELSE NULL.
Мы неоднократно убеждались, что пропущенные условия, пустые результаты агрегаций и опущенные ветки ELSE приводят к появлению NULL. Работа с отсутствующими значениями — это самая коварная область реляционной теории, порождающая трехзначную логику и ломающая базовую интуицию инженеров. Пришло время раз и навсегда расставить точки над NULL в следующей статье: 12. Работа с NULL в SQL.