14. Сложная фильтрация и CASE
🧭 Введение: когда простой WHERE уже не спасает
Пока условия простые, хватает
Но в боевых задачах фильтры быстро усложняются: разные статусы, даты, исключения, приоритеты, сегменты.
WHERE status = 'paid'.Но в боевых задачах фильтры быстро усложняются: разные статусы, даты, исключения, приоритеты, сегменты.
Здесь нужны вложенные логические конструкции и
Именно на этом этапе чаще всего появляются тихие баги из-за приоритета операторов и неочевидной логики.
CASE WHEN, чтобы превратить бизнес-правила в понятный SQL.Именно на этом этапе чаще всего появляются тихие баги из-за приоритета операторов и неочевидной логики.
💡 Совет:
Перед написанием запроса формулируйте правило человеческим языком, а потом разбивайте на отдельные логические блоки в SQL.
✅ Вывод:
Сложная фильтрация — это не «хитрый синтаксис», а точный перенос бизнес-логики в запрос.
⚠️ Проблема -> решение
Типичная проблема:
- условия пишутся «в одну строку»,
- теряется контроль приоритета
AND/OR/NOT, CASEиспользуется безELSE, и часть строк получает неожиданныйNULL.
Последствия:
- сегменты пользователей считаются неверно,
- API возвращает неожиданные категории,
- ревью и поддержка запроса становятся дорогими.
Решение:
- явно группировать условия скобками,
- проверять приоритет операторов и граничные кейсы,
- строить вычисляемые поля через
CASEс явнымELSE.
🟢 Если совсем просто:
Сначала делим логику на блоки, потом пишем SQL и проверяем поведение на крайних примерах.
🎯 Как понять, что этап прошёл успешно:
Вы можете объяснить, почему каждая строка попала в результат и какую ветку
CASE она прошла.🛠️ Чем помогает и как работает
Сложная фильтрация нужна почти в каждом production-проекте:
- правила скидок и приоритетов заказов;
- сегментация пользователей для CRM;
- риск-оценка транзакций;
- классификация событий в отчётах.
🟢 Если совсем просто:
WHERE отбирает строки, CASE присваивает им категории и вычисляемые значения.🎯 Как понять, что этап прошёл успешно:
Вы умеете разделить логику на фильтрацию, классификацию и вычисляемые поля.
Чем помогает:
- вложенные условия выражают сложные бизнес-правила;
- правильный приоритет операторов предотвращает скрытые ошибки;
CASEсоздаёт прозрачные категориальные поля;- вычисляемые поля упрощают API и дашборды.
Как это работает:
- Шаг 1: фиксируем логические группы условий.
- Шаг 2: ставим скобки для контроля приоритета.
- Шаг 3: добавляем
CASEдля классификации. - Шаг 4: валидируем
ELSEи пограничные случаи. - Шаг 5: проверяем читаемость и производительность.
✅ Вывод:
Качество сложного SQL определяется ясной логикой, а не длиной запроса.
📚 Ключевые термины (простыми словами)
Перед практикой синхронизируем словарь.
🟢 Если совсем просто:
Термины ниже отвечают на вопрос «как не потерять смысл в длинном условии».
🎯 Как понять, что этап прошёл успешно:
Вы понимаете, где фильтрация, где классификация, и где риск логической ошибки.
- Nested conditions (вложенные условия) — логические блоки с несколькими
AND/OR/NOT. - Operator precedence (приоритет операторов) — порядок вычисления
NOT->AND->OR. - CASE WHEN — условная конструкция для вычисления категории/значения.
- Computed field (вычисляемое поле) — поле, которое считается в запросе, а не хранится в таблице.
- Short-circuit thinking — подход, где условия проверяются от более критичных к менее критичным.
- Boolean expression — логическое выражение, возвращающее
TRUE/FALSE/UNKNOWN. - Default branch (
ELSE) — ветка по умолчанию вCASE. - Logical grouping — осознанное объединение условий в отдельные блоки через скобки.
✅ Вывод:
Эти понятия дают базу для безопасной сложной фильтрации в production.
🧩 1. Вложенные условия в WHERE
Когда условий много, запрос нужно читать как набор логических блоков, а не как одну длинную строку.
Скобки здесь не «лишние», а инструмент контроля бизнес-смысла.
Скобки здесь не «лишние», а инструмент контроля бизнес-смысла.
🟢 Если совсем просто:
Один блок = одно бизнес-правило, несколько блоков = итоговая фильтрация.
🎯 Как понять, что этап прошёл успешно:
Вы можете вслух прочитать условие запроса как текстовое правило без двусмысленности.
Назначение:
Явно структурировать сложную фильтрацию по логическим группам.
Простыми словами:
Сначала объединяем близкие условия в блоки, потом связываем блоки между собой.
Для новичка:
Не экономьте на скобках в сложных выражениях — это снижает риск ошибки.
Аналогия:
Это как юридический договор: пункты должны быть явно сгруппированы, иначе смысл спорный.
Пример:
SELECT o.id, o.user_id, o.total_amountFROM orders oWHERE ( o.status = 'paid' AND o.total_amount >= 1000 ) OR ( o.status = 'processing' AND o.created_at >= DATE '2026-03-01' );🔎 Как это происходит на практике:
- Контекст: витрина «важные заказы» для оператора.
- Действия: собирают два независимых правила и объединяют через
OR. - Результат: каждая группа условий читается отдельно и тестируется отдельно.
Характеристики:
- хорошо документирует бизнес-логику;
- снижает риск случайного изменения смысла;
- упрощает ревью и отладку.
Когда использовать:
Всегда, когда в
WHERE больше одного логического блока.✅ Вывод:
Скобки в сложной фильтрации — обязательная часть инженерного качества.
⚖️ 2. Приоритет операторов AND / OR / NOT
SQL вычисляет логические операторы по приоритету:
NOT -> AND -> OR.Если не учитывать это явно, можно получить корректный синтаксис, но неверный результат.
Поэтому в боевых запросах приоритет лучше фиксировать скобками.
Поэтому в боевых запросах приоритет лучше фиксировать скобками.
🟢 Если совсем просто:
Без скобок SQL может понять условие не так, как вы задумали.
🎯 Как понять, что этап прошёл успешно:
Вы можете переписать любое сложное условие с явными скобками и тем же смыслом.
Назначение:
Избежать скрытых логических ошибок в многоусловных фильтрах.
Простыми словами:
A OR B AND C = A OR (B AND C), а не (A OR B) AND C.Для новичка:
Если в выражении есть и
AND, и OR — почти всегда ставьте скобки.Аналогия:
Это как математика: сначала умножение, потом сложение.
Пример:
-- Опасная формаSELECT u.id, u.emailFROM users uWHERE u.is_vip = true OR u.region = 'EU' AND u.is_blocked = false; -- Явная и безопасная формаSELECT u.id, u.emailFROM users uWHERE (u.is_vip = true OR u.region = 'EU') AND u.is_blocked = false;🧨 Антипример: тестовые строки ломают сегмент
-- Хотели: VIP или EU, но не blocked, и исключить тестовые emailWHERE (u.is_vip = true OR u.region = 'EU') AND u.is_blocked = false AND u.email NOT ILIKE '%test%'Если убрать первые скобки, смысл меняется, и часть
blocked записей может пройти.🕳️ 3-valued logic (TRUE/FALSE/UNKNOWN) — тихая ловушка
В SQL логические выражения могут возвращать
UNKNOWN, когда участвует NULL.WHERE оставляет только TRUE, а FALSE и UNKNOWN отбрасываются.Мини-пример:
-- Если last_order_at = NULL, сравнение даст UNKNOWNWHERE last_order_at >= DATE '2026-03-01'✅ Практика:
- для nullable-дат используйте явную логику
last_order_at IS NOT NULL AND last_order_at >= ...; COALESCEприменяйте только когда это действительно бизнес-значение по умолчанию.
🔎 Как это происходит на практике:
- Контекст: сегментация VIP-пользователей.
- Действия: команда добавляет блокировку в общий фильтр.
- Результат: без скобок часть строк ошибочно проходит.
Характеристики:
- ошибка не вызывает падение запроса;
- баг часто заметен только в бизнес-метриках;
- легко предотвратить дисциплиной скобок.
Когда использовать:
При любом сочетании
AND/OR/NOT в одном условии.✅ Вывод:
Явный приоритет — базовая защита от самых дорогих логических багов.
🧮 3. CASE WHEN для классификации
CASE WHEN превращает набор условий в категорию или вычисляемое значение.Это ключевой инструмент для отчётов, статусов и риск-оценки.
🟢 Если совсем просто:
CASE = «если условие выполнено, присвой метку/значение».🎯 Как понять, что этап прошёл успешно:
У каждой строки есть понятная категория, а не неожиданный
NULL.Назначение:
Классифицировать строки и вычислять бизнес-поля в одном запросе.
Простыми словами:
CASE проверяет условия по порядку и берёт первую подходящую ветку.Для новичка:
Всегда добавляйте
ELSE, чтобы явно контролировать «прочие» случаи.⚠️ CASE — это выражение, а не «магический if»
CASEвSELECTклассифицирует или считает поле;CASEвWHEREтоже просто вычисляет выражение, которое должно статьTRUE.
Пример условия включения через
CASE:WHERE CASE WHEN u.is_blocked THEN false WHEN u.is_vip THEN true WHEN u.region = 'EU' AND u.is_active THEN true ELSE false ENDВ большинстве случаев читабельнее обычная логика со скобками, но этот паттерн иногда полезен.
Аналогия:
Это как правила сортировки заявок: критичные, средние, обычные.
Пример:
SELECT o.id AS order_id, o.total_amount, o.status, CASE WHEN o.status = 'chargeback' THEN 'critical' WHEN o.status = 'paid' AND o.total_amount >= 5000 THEN 'high' WHEN o.status = 'paid' THEN 'normal' ELSE 'review' END AS risk_tierFROM orders o;📊 Условные метрики через CASE (частый паттерн)
SELECT COUNT(*) AS total_users, SUM(CASE WHEN u.is_vip THEN 1 ELSE 0 END) AS vip_users, SUM(CASE WHEN u.region = 'EU' AND u.is_blocked = false THEN 1 ELSE 0 END) AS eu_unblocked_usersFROM users u;Правило:
SUM(CASE ... THEN 1 ELSE 0 END) - это счётчик по условию и обычно читается проще, чем набор отдельных запросов.🔎 Как это происходит на практике:
- Контекст: операторский дашборд по рискам.
- Действия: статусы и суммы переводят в единый
risk_tier. - Результат: фронтенд получает готовую категорию без дублирования логики.
Характеристики:
- порядок веток имеет значение;
ELSEпредотвращает неожиданныйNULL;- удобно для вычисления категорий в API-ответе.
Когда использовать:
Когда в выборке нужна человекочитаемая бизнес-классификация.
✅ Вывод:
CASE WHEN — стандартный способ делать бизнес-логику явно и прозрачно.🧱 4. Вычисляемые поля и повторное использование логики
Вычисляемые поля в
Но если логика сложная, её лучше вынести в CTE, чтобы не дублировать в нескольких местах.
SELECT позволяют не хранить производные данные в таблице.Но если логика сложная, её лучше вынести в CTE, чтобы не дублировать в нескольких местах.
🟢 Если совсем просто:
Считаем поле один раз, потом используем его дальше в запросе.
🎯 Как понять, что этап прошёл успешно:
Логика не копируется по 2-3 раза, а изменяется в одном месте.
Назначение:
Сделать сложный SQL поддерживаемым и читаемым.
Простыми словами:
CTE помогает сначала посчитать поле, потом фильтровать/сортировать по нему.
Для новичка:
Не пытайтесь вставлять огромный
CASE в каждый блок — лучше вынести в WITH.Аналогия:
Это как именовать сложную формулу переменной, а не писать её каждый раз заново.
Пример:
WITH scored_orders AS ( SELECT o.id, o.user_id, o.status, o.total_amount, CASE WHEN o.status = 'chargeback' THEN 100 WHEN o.status = 'paid' AND o.total_amount >= 5000 THEN 70 WHEN o.status = 'paid' THEN 30 ELSE 10 END AS risk_score FROM orders o)SELECT s.id AS order_id, s.user_id, s.risk_scoreFROM scored_orders sWHERE s.risk_score >= 50ORDER BY s.risk_score DESC;🔎 Как это происходит на практике:
- Контекст: риск-модель для ручной проверки заказов.
- Действия: сначала считают
risk_score, потом фильтруют и сортируют. - Результат: логика ясная и переиспользуемая.
Характеристики:
- снижает дублирование кода;
- упрощает поддержку;
- делает ревью проще.
Когда использовать:
Когда одно и то же вычисляемое условие нужно в нескольких местах запроса.
✅ Вывод:
CTE + вычисляемые поля — базовый паттерн для поддерживаемого сложного SQL.
🧠 5. Сложные логические конструкции в реальных правилах
Иногда одно правило зависит сразу от нескольких факторов: статуса, времени, суммы, региона и исключений.
В таком случае полезно разделять «включающие» и «исключающие» условия.
В таком случае полезно разделять «включающие» и «исключающие» условия.
🟢 Если совсем просто:
Сначала пишем, кто может попасть в сегмент, потом отдельно — кто должен быть исключён.
🎯 Как понять, что этап прошёл успешно:
Запрос легко проверяется на кейсах «должен попасть / не должен попасть».
Назначение:
Собрать сложную фильтрацию без потери читаемости и контроля.
Простыми словами:
Логика «include/exclude» в SQL снижает количество спорных интерпретаций.
Для новичка:
Пишите исключения отдельным блоком
AND NOT (...).Аналогия:
Это как правила допуска на мероприятие: кто проходит и кто исключается.
Пример:
SELECT u.id AS user_id, u.emailFROM users uWHERE ( (u.is_vip = true AND u.is_active = true) OR (u.region = 'EU' AND u.last_order_at >= DATE '2026-02-01') ) AND NOT ( u.is_blocked = true OR u.email LIKE '%test%' );🔎 Как это происходит на практике:
- Контекст: сегмент для дорогой рекламной кампании.
- Действия: формируют условия включения и список исключений.
- Результат: меньше ложноположительных записей в кампании.
Характеристики:
- логика читается как бизнес-требование;
- удобно тестировать по таблице кейсов;
- меньше риск «забыть исключение».
Когда использовать:
Когда фильтр содержит одновременно сложные условия включения и исключения.
✅ Вывод:
Явная структура include/exclude делает сложные фильтры надежными и проверяемыми.
⚡ 6. Производительность сложных фильтров и CASE
Даже корректный по логике запрос может быть медленным, если условия неселективны или написаны неудачно.
Особенно это касается больших таблиц и выражений с функциями над колонками.
Особенно это касается больших таблиц и выражений с функциями над колонками.
⚡ Частая причина медленных фильтров
WHERE lower(email) = 'a@b.com'Такой фильтр часто не использует индекс по
Варианты:
email.Варианты:
- хранить
emailв нормализованном виде; - сравнивать без функции по колонке;
- при необходимости использовать специализированный индекс под выражение.
🟢 Если совсем просто:
Чем раньше и точнее вы режете набор данных, тем дешевле запрос.
🎯 Как понять, что этап прошёл успешно:
Вы можете объяснить, какие части условия используют индексы, а какие нет.
Назначение:
Сделать сложную фильтрацию предсказуемой по времени выполнения.
Простыми словами:
Ранние селективные условия + аккуратные выражения = быстрый запрос.
Для новичка:
Функции/касты в
WHERE (lower(email), created_at::text) часто мешают индексному плану.Аналогия:
Сначала отобрать нужные папки по индексу, потом читать документы внутри.
Пример:
SELECT o.id, o.user_id, CASE WHEN o.status = 'paid' AND o.total_amount >= 5000 THEN 'priority' ELSE 'normal' END AS queue_typeFROM orders oWHERE o.created_at >= DATE '2026-03-01' AND o.created_at < DATE '2026-03-08' AND o.status IN ('paid', 'processing');🔎 Как это происходит на практике:
- Контекст: очередь обработки заказов в near-real-time.
- Действия: сначала сужают период и статусы, потом считают
CASE. - Результат: стабильная латентность даже на больших объёмах.
Характеристики:
- селективность фильтра критична;
- индексы на фильтрах и ключах обязательны;
EXPLAINпомогает увидеть слабые места.
Когда использовать:
Всегда для сложных фильтров на таблицах с большим объёмом данных.
✅ Вывод:
Производительность сложного SQL — это инженерная дисциплина, а не удача.
🆚 Сравнение: фильтрация и CASE
| Конструкция | Что решает | Когда лучше | Риск |
|---|---|---|---|
Скобки в WHERE | Явный порядок логики | AND/OR в одном условии | Потеря смысла без скобок |
CASE WHEN | Классификация и вычисляемые поля | Отчёты, API, очереди | NULL без ELSE |
CTE + CASE | Повторное использование сложной логики | Длинные запросы | Лишняя сложность без необходимости |
AND NOT (...) | Явные исключения | include/exclude сегменты | Ошибка при смешивании исключений |
✅ Вывод:
Чистая структура условий важнее «короткого» SQL.
🧠 Must-Know (запомнить)
- Приоритет операторов:
NOT->AND->OR. - Если в фильтре есть
ANDиOR, ставьте явные скобки. - В
CASEпорядок веток имеет значение. CASEбезELSEможет вернуть неожиданныйNULL.- Вычисляемую бизнес-логику лучше выносить в CTE при повторном использовании.
- Фильтры include/exclude лучше оформлять отдельными блоками.
- Функции и касты над колонками в
WHEREмогут ломать индексный путь.
🧷 NULL-safe проверки
= NULLникогда не сработает -> используйтеIS NULL.!= NULLтоже не сработает -> используйтеIS NOT NULL.- Для PostgreSQL есть NULL-safe оператор сравнения:
a IS NOT DISTINCT FROM b.
-- NULL-safe равенство (PostgreSQL)a IS NOT DISTINCT FROM b✅ Вывод:
Эти правила закрывают большинство ошибок в сложной фильтрации.
❌ Частые мифы
❌ Миф: Скобки в
WHERE можно не писать, SQL и так поймёт.
✅ Как правильно: в сложной логике скобки обязательны для контроля смысла.
📎 Почему это важно: синтаксис может быть валиден, но результат — неверный.❌ Миф:
CASE нужен только для красивого вывода.
✅ Как правильно: CASE — рабочий инструмент бизнес-классификации и вычислений.
📎 Почему это важно: без него логика расползается по приложению и отчётам.❌ Миф:
ELSE в CASE не обязателен.
✅ Как правильно: лучше всегда явно задавать ветку по умолчанию.
📎 Почему это важно: иначе часть данных получает NULL без явного контроля.❌ Миф: Если фильтр логически корректен, о производительности думать не надо.
✅ Как правильно: на больших таблицах форма условий и селективность критичны.
📎 Почему это важно: иначе SLA API и стоимость инфраструктуры ухудшаются.
🎤 Часто спрашивают на собеседованиях
❓ Вопрос: Какой приоритет у
NOT, AND, OR в SQL?
✅ Ответ: сначала NOT, затем AND, затем OR; в сложных условиях лучше ставить явные скобки.❓ Вопрос: Зачем нужен
CASE WHEN?
✅ Ответ: для вычисления категорий и значений по условиям прямо в запросе.❓ Вопрос: Почему важно писать
ELSE в CASE?
✅ Ответ: чтобы контролировать ветку по умолчанию и избежать неожиданного NULL.❓ Вопрос: Как сделать сложный фильтр читаемым?
✅ Ответ: разбить условия на логические блоки, использовать скобки и осмысленные алиасы.
❓ Вопрос: Что такое вычисляемое поле?
✅ Ответ: поле, которое считается в
SELECT (например, через CASE) и не хранится физически в таблице.❓ Вопрос: Как оптимизировать сложную фильтрацию?
✅ Ответ: ранние селективные условия, индексы на ключевых фильтрах и проверка плана через
EXPLAIN.🚨 Типичные ошибки
- Писать длинное условие
WHEREбез скобок. - Предполагать неверный приоритет
AND/OR. - Забывать
ELSEвCASE. - Дублировать один и тот же сложный
CASEв нескольких местах. - Смешивать условия включения и исключения без явной структуры.
- Использовать функции/касты над индексируемыми полями в фильтре.
- Проверять только «happy path» и не тестировать граничные кейсы.
✅ Вывод:
Главные ошибки в этой теме — логические, а не синтаксические.
✅ Best Practices
- Группируйте условия по смыслу и фиксируйте скобками.
- Явно документируйте include/exclude логику внутри
WHERE. - В
CASEвсегда добавляйтеELSE. - Для сложной повторяемой логики используйте CTE.
- Пишите вычисляемые поля с понятными alias для API.
- Тестируйте фильтр на граничных примерах до релиза.
- Проверяйте план выполнения для тяжёлых запросов.
✅ Вывод:
Хороший сложный SQL — это сочетание правильной логики, читаемости и предсказуемой производительности.
🏁 Заключение
Сложная фильтрация и
Именно здесь запрос либо становится надежным, либо превращается в источник тихих багов.
CASE — это ядро прикладного SQL в аналитике и backend-разработке.Именно здесь запрос либо становится надежным, либо превращается в источник тихих багов.
Если вы контролируете приоритет операторов, аккуратно строите логические блоки и осознанно используете вычисляемые поля,
вы получаете стабильные и объяснимые результаты.
вы получаете стабильные и объяснимые результаты.
✅ Вывод:
Сильный инженер умеет не просто написать
WHERE, а доказать корректность каждой ветки фильтрации.