NOT EXISTS и anti-join в SQL: как найти пользователей без события
Практический разбор NOT EXISTS и anti-join: пользователи без покупки, заказы без возврата и сегменты, которые не дошли до следующего шага.
Содержание статьи
Сегмент «не купили» нельзя получить простым подсчётом пользователей без строки в отчёте. После LEFT JOIN легко получить дубли, перепутать NULL с пустым значением или случайно превратить соединение в INNER JOIN фильтром в WHERE. Anti-join помогает выразить отрицательную связь явно. Разберём вопрос на одном запросе: Как корректно найти тех, у кого не произошло событие?
Сначала определим задачу
Как корректно найти тех, у кого не произошло событие?
Чтобы найти пользователей, зарегистрированных за неделю, но без activation в первые семь дней, ограничь и базовую когорту, и окно события. «Никогда не активировался» и «не активировался в первые семь дней» — разные условия. Большинство ошибок здесь методологические, а не синтаксические.
Используй NOT EXISTS как основной читаемый способ отрицательной проверки. LEFT JOIN anti-join оставь для случаев, когда нужны поля второй таблицы или аудит совпадений. В любом варианте явно фиксируй окно и ключ связи.
Как работает конструкция
Anti-join возвращает строки левой таблицы, для которых не найдено совпадение в правой. В SQL это обычно NOT EXISTS или LEFT JOIN ... WHERE right.key IS NULL. Первый вариант прямо говорит о проверке отсутствия, второй удобен, если нужно показать поля и диагностировать связь.
Сначала сформируй левую базу и назови её зерно. Затем опиши, какая строка справа считается совпадением и в каком временном окне. Проверь пользователя с несколькими событиями, событием вне окна и NULL id.
- Зафиксируй одну строку результата.
- Отдели условия по исходным строкам от условий по рассчитанным значениям.
- Назови период, ключи связи и правило для повторов.
Запрос по шагам
Названия таблиц здесь условные. При переносе сохрани зерно и бизнес-условия, а не буквальный синтаксис примера.
SELECT u.user_id, u.channel
FROM users u
WHERE u.registered_at >= DATE '2026-07-01'
AND u.registered_at < DATE '2026-07-08'
AND NOT EXISTS (
SELECT 1 FROM events e
WHERE e.user_id = u.user_id AND e.event_name = 'purchase'
AND e.occurred_at >= u.registered_at
AND e.occurred_at < u.registered_at + INTERVAL '14 days'
);Проверка результата
Собери пять-десять строк, где ответ можно получить вручную. Добавь NULL, дубль, объект без связанной записи и значение на границе периода. Затем сравни число строк и уникальных ключей до и после спорного шага.
Выполненный без ошибки запрос ещё не является правильным. Другой аналитик должен суметь восстановить из него источник, фильтр, grain и ожидаемое поведение на крайних случаях.
| Часть | Вопрос | Пример |
|---|---|---|
| левая база | кто должен попасть? | новые пользователи |
| совпадение | какое событие? | purchase |
| окно | до какой даты? | 14 дней |
| ключ | как связать? | user_id |
Границы и альтернативы
Условие правой таблицы в WHERE после LEFT JOIN превращает его в INNER JOIN. Проверка right.id IS NULL должна использовать настоящий not-null ключ. Не делай NOT EXISTS по всей истории, если вопрос про конкретную когорту или период.
Используй NOT EXISTS как основной читаемый способ отрицательной проверки. LEFT JOIN anti-join оставь для случаев, когда нужны поля второй таблицы или аудит совпадений. В любом варианте явно фиксируй окно и ключ связи.
Пользователь остаётся в базе, если совпадение не найдено в заданном окне.
Задание для самостоятельной проверки
Собери список новых пользователей без покупки в 14 дней. Затем разбей его по каналу и проверь, что сумма сегментов совпадает с общей базой. Добавь событие на 15-й день: оно не должно убрать пользователя из результата.
- Сначала запиши ожидаемый результат словами.
- Проверь запрос на маленькой контрольной выборке.
- Объясни, что запрос считает и чего не доказывает.
Решение для команды
Используй NOT EXISTS как основной читаемый способ отрицательной проверки. LEFT JOIN anti-join оставь для случаев, когда нужны поля второй таблицы или аудит совпадений. В любом варианте явно фиксируй окно и ключ связи.
Материалы по теме

SQL CASE, COALESCE и NULL: как не сломать сегменты и метрики
Разбираем NULL, CASE и COALESCE на задачах аналитика: как группировать пустые значения, создавать сегменты и показывать ноль вместо пропущенных данных.

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