SQL CASE, COALESCE и NULL: как не сломать сегменты и метрики
Разбираем NULL, CASE и COALESCE на задачах аналитика: как группировать пустые значения, создавать сегменты и показывать ноль вместо пропущенных данных.
Содержание статьи
Пустое значение в отчёте редко остаётся просто пустым. Оно может означать неизвестный канал, отсутствие платежа, ещё не наступивший период или ошибку загрузки. SQL различает NULL и обычный текст, поэтому неосторожное условие легко убирает часть пользователей из выборки или превращает “нет данных” в ложный ноль.
Коротко
NULL означает отсутствие известного значения, а не пустую строку и не ноль. CASE создаёт новое значение по условиям, COALESCE выбирает первое непустое значение. Вместе они помогают сделать бизнес-правила явными и сохранить правильный знаменатель метрики.
- Для проверки NULL используй
IS NULLилиIS NOT NULL, а не= NULL. COALESCE(value, 0)превращает пропуск в ноль только там, где это действительно означает ноль.CASEполезен для сегментов, флагов и читаемых категорий.- Не смешивай неизвестный канал и органический канал без отдельного решения.
- Проверяй, изменился ли знаменатель после фильтра по NULL.
NULL — не пустая строка
В таблице users у части пользователей может быть неизвестный channel. Это не то же самое, что channel = '', и не то же самое, что channel = 'organic'. NULL говорит: значение неизвестно или не было записано.
| Значение | Смысл | Как проверить |
|---|---|---|
| NULL | значение отсутствует или неизвестно | channel IS NULL |
| пустая строка | поле записано, но строка пустая | channel = '' |
| 0 | числовой ноль | amount = 0 |
select
count(*) filter (where channel is null) as missing_channel,
count(*) filter (where channel = '') as empty_channel,
count(*) as all_users
from users;Почему channel = NULL не работает
SQL использует трёхзначную логику: условие может быть TRUE, FALSE или UNKNOWN. Сравнение любого значения с NULL не возвращает TRUE, поэтому where channel = null не найдёт пропуски. Нужна специальная проверка IS NULL.
select
user_id,
signup_date,
channel
from users
where channel is null
order by signup_date, user_id;Если после добавления WHERE количество пользователей стало меньше, чем ожидалось, отдельно посмотри NULL-группу. Пропуски часто прячутся именно там.
CASE: создать сегмент из условия
CASE возвращает значение в зависимости от условий. Он подходит, когда в исходной таблице нет готовой бизнес-категории: можно разделить пользователей по каналу, возрасту когорты или активности. Важно задать ELSE, иначе все неописанные случаи станут NULL.
select
user_id,
channel,
case
when channel = 'paid' then 'paid'
when channel in ('organic', 'referral') then 'owned'
when channel is null then 'unknown'
else 'other'
end as channel_group
from users
order by user_id;- Условия CASE проверяются сверху вниз.
- Первое совпавшее условие определяет результат.
ELSEзащищает от неожиданных NULL в новой колонке.- Категории должны быть взаимоисключающими, если потом считаешь их сумму.
COALESCE: показать отсутствие как ноль
После LEFT JOIN у пользователя без платежа поле amount будет NULL. Для отчёта о выручке это обычно нужно показать как 0, но только после того, как ты сохранил пользователя в знаменателе. COALESCE выбирает первое значение, которое не является NULL.
select
u.user_id,
u.channel,
coalesce(sum(p.amount), 0) as revenue
from users as u
left join payments as p using (user_id)
group by u.user_id, u.channel
order by revenue desc, u.user_id;Для суммы платежей отсутствие строк часто можно показать нулём. Для даты последнего платежа NULL обычно означает “платежа не было” и не должен превращаться в произвольную дату.
Как не потерять сегмент в GROUP BY
GROUP BY соберёт все NULL-значения в одну группу. Это полезно для контроля качества, но не всегда хорошо для бизнес-отчёта. Сначала покажи unknown отдельно, оцени размер группы и только потом решай, можно ли распределить её по другим категориям.
| Ситуация | Рабочее решение |
|---|---|
| Канал неизвестен | показать unknown и исправить трекинг |
| Платежей нет | COALESCE(sum(amount), 0) для денежной метрики |
| Дата возврата неизвестна | оставить NULL и не подставлять фиктивную дату |
| Категория не предусмотрена | CASE с явным ELSE other |
select
coalesce(channel, 'unknown') as channel_group,
count(*) as users_count
from users
group by coalesce(channel, 'unknown')
order by users_count desc;Частые ошибки
Большинство проблем возникает не из-за синтаксиса, а из-за подмены смысла. Ноль, неизвестность и отсутствие строки могут выглядеть одинаково в таблице, но приводят к разным продуктовым выводам.
- Проверять NULL через
= NULL. - Использовать COALESCE для дат и создавать ложные значения.
- Забыть ELSE в CASE и получить новые пропуски.
- Отфильтровать NULL до расчёта и уменьшить знаменатель.
- Склеить unknown с organic без проверки качества канала.
- Считать сумму сегментов и не заметить, что часть строк ушла в NULL.
NULL в долях, средних и сравнениях
NULL не равен нулю и не равен пустой строке. В арифметике он обычно распространяется: сумма с NULL может стать NULL, а деление на NULL не даёт долю. count(column) пропускает NULL, count(*) считает строку. Поэтому запрос может выглядеть корректно, но использовать разные базы в числителе и знаменателе.
Перед coalesce задай смысл отсутствия. Для скидки NULL иногда означает «скидки не было», и ноль оправдан. Для revenue NULL означает «сумма неизвестна», и подстановка нуля занизит итог. Для channel лучше использовать unknown, чтобы группа не исчезла из отчёта.
В ratio-метриках используй nullif(denominator, 0). Это возвращает NULL для пустой базы и позволяет отчёту показать «нет наблюдений», а не бесконечность или искусственный ноль. Рядом с формулой подпиши, что именно исключено из знаменателя.
select
channel,
count(*) filter (where status = 'paid') as paid_orders,
count(*) as all_orders,
round(
count(*) filter (where status = 'paid')::numeric
/ nullif(count(*), 0),
4
) as paid_share
from orders
group by channel
order by channel;CASE как контракт сегмента
CASE часто начинается как маленькое условие и превращается в определение бизнес-сегмента. Запиши порядок правил от специфичного к общему и добавь else unknown, если новые значения должны быть заметны. else low смешивает реальные низкие значения с невалидными строками.
Проверь границы на контрольных значениях: ровно 0, ровно 1000, NULL, отрицательное значение и новая категория. Если условия пересекаются, первый подходящий WHEN выигрывает. Это должно быть осознанным приоритетом, а не случайностью расположения строк.
Если один и тот же CASE используется в нескольких отчётах, вынеси его в view или справочник. Иначе продуктовая команда будет получать разные сегменты для одинакового пользователя из-за небольшой разницы в SQL.
select
user_id,
case
when revenue is null then 'unknown'
when revenue < 0 then 'invalid'
when revenue >= 5000 then 'high'
when revenue >= 1500 then 'medium'
else 'low'
end as revenue_segment
from user_revenue;Мини-практика и следующий шаг
Найди пользователей с неизвестным channel, затем создай channel_group через CASE. После этого присоедини payments и выведи ноль для пользователей без платежей. В конце сравни сумму пользователей по группам с общим количеством строк.
Материалы по теме

SQL-проверки качества данных: дубли, пропуски и скачки метрик
Практический чеклист SQL-проверок перед дашбордом: найти дубли, пропущенные ключи, события без пользователей и неожиданные скачки дневного объёма.

SQL JOIN для аналитика: как соединять users, events и payments
Понятное объяснение INNER JOIN и LEFT JOIN: как связать пользователей с событиями и платежами, не потерять сегменты и не завысить метрику после соединения.
Собеседование BI-аналитика: дашборды, Excel, SQL и бизнес-кейсы
Практический гайд по собеседованию BI-аналитика: как проектировать дашборд, выбирать KPI, проверять данные, отвечать про Excel и защищать вывод перед бизнесом.