Нужно найти пользователей, которые ни разу не заходили в приложение. Вы пишете запрос с NOT IN, запускаете его и получаете пустой результат. Хотя пользователи без сессий точно есть.
Причиной может оказаться единственный NULL в подзапросе. Разберём ситуацию на небольшом примере.
Задача: найти пользователей без сессий
Есть две таблицы.
Пользователи — users:
Сессии — sessions:
У Анны есть сессия. Борис и Вера ещё не заходили. Вторая сессия не связана с зарегистрированным пользователем: например, это анонимное посещение.
Ожидаемый результат:
Запрос, который выглядит правильным
В PostgreSQL пример можно выполнить целиком: временные наборы данных создаются внутри запроса через WITH, отдельные таблицы не нужны.
WITH users (id, name) AS (
VALUES
(1, 'Анна'),
(2, 'Борис'),
(3, 'Вера')
),
sessions (id, user_id) AS (
VALUES
(101, 1),
(102, NULL)
)
SELECT u.id, u.name
FROM users AS u
WHERE u.id NOT IN (
SELECT s.user_id
FROM sessions AS s
)
ORDER BY u.id;
Этот запрос вернёт ноль строк.
Подзапрос возвращает значения 1 и NULL. Для Бориса проверку можно представить так:
2 <> 1 AND 2 <> NULL
Первое сравнение истинно. Результат второго неизвестен.
Что означает NULL
NULL обозначает отсутствие известного значения. Это не ноль, не пустая строка и не отдельное значение, которое можно сравнить обычным равенством.
Например:
SELECT
2 = NULL AS equal_result,
2 <> NULL AS unequal_result,
NULL IS NULL AS null_check;
Первые два выражения дают NULL, последнее — TRUE. Для проверки отсутствующего значения используются IS NULL и IS NOT NULL. Документация PostgreSQL о сравнениях.
Условие WHERE сохраняет только строки, для которых выражение истинно. Неизвестный результат для этого не подходит.
Поэтому Борис и Вера исчезают из результата NOT IN. Анна тоже не проходит условие: её идентификатор найден в сессиях.
Решение через NOT EXISTS
Для нашей задачи удобно проверить отсутствие связанной строки:
WITH users (id, name) AS (
VALUES
(1, 'Анна'),
(2, 'Борис'),
(3, 'Вера')
),
sessions (id, user_id) AS (
VALUES
(101, 1),
(102, NULL)
)
SELECT u.id, u.name
FROM users AS u
WHERE NOT EXISTS (
SELECT 1
FROM sessions AS s
WHERE s.user_id = u.id
)
ORDER BY u.id;
Теперь результат соответствует условию:
Подзапрос проверяет наличие сессии для конкретного пользователя. Если совпадающих строк нет, NOT EXISTS возвращает истину.
Сессия с user_id = NULL не совпадает ни с одним из наших идентификаторов и не мешает проверке остальных пользователей. Поведение EXISTS и особенности NOT IN описаны в документации PostgreSQL о подзапросах.
Зачем писать SELECT 1
В этой проверке важен сам факт существования строки. Число 1 не является идентификатором пользователя и не возвращается в итоговый результат.
Это распространённый способ записи проверки существования.
Можно ли оставить NOT IN
Да. В нашем примере достаточно исключить отсутствующие идентификаторы из подзапроса:
SELECT u.id, u.name
FROM users AS u
WHERE u.id NOT IN (
SELECT s.user_id
FROM sessions AS s
WHERE s.user_id IS NOT NULL
)
ORDER BY u.id;
Этот фрагмент рассчитан на уже существующие таблицы users и sessions. Для запуска на демонстрационных данных используйте тот же блок WITH, что и выше.
Здесь users.id всегда заполнен. Если сравниваемое поле внешнего запроса тоже допускает NULL, отдельно определите, должны ли такие строки попадать в результат.
Не стоит заменять NULL случайным числом через COALESCE: выбранное число может оказаться реальным идентификатором.
Как объяснить решение на собеседовании
Хорошее объяснение включает три пункта:
Нужно найти пользователей, для которых отсутствует связанная сессия.
NOT IN требует внимания к NULL в сравниваемых данных.
NOT EXISTS позволяет явно записать проверку отсутствия связи.
Утверждать, что NOT EXISTS всегда быстрее, не нужно. Производительность сравнивают на конкретных данных и планах выполнения.
Проверьте себя
Измените демонстрационные данные:
Кто останется в результате запроса с NOT EXISTS?
Ответ: только Вера. Повторная сессия Анны не меняет результат проверки существования, а у Бориса теперь есть связанная запись.
Главное
Когда задача звучит как «найти записи, для которых нет связанных записей», рассмотрите NOT EXISTS.
Если используете NOT IN, проверьте, могут ли сравниваемые значения содержать NULL. Небольшой пример с отсутствующим значением помогает заметить ошибку до работы с настоящими данными.
Хотите потренироваться в решении подобных задач? Посмотрите подготовку к собеседованиям по SQL. Если подзапросы пока вызывают затруднения, начните с практикума SQL с нуля.
Нужно найти пользователей, которые ни разу не заходили в приложение. Вы пишете запрос с
NOT IN, запускаете его и получаете пустой результат. Хотя пользователи без сессий точно есть.Причиной может оказаться единственный
NULLв подзапросе. Разберём ситуацию на небольшом примере.Задача: найти пользователей без сессий
Есть две таблицы.
Пользователи —
users:id
name
1
Анна
2
Борис
3
Вера
Сессии —
sessions:id
user_id
101
1
102
NULL
У Анны есть сессия. Борис и Вера ещё не заходили. Вторая сессия не связана с зарегистрированным пользователем: например, это анонимное посещение.
Ожидаемый результат:
id
name
2
Борис
3
Вера
Запрос, который выглядит правильным
В PostgreSQL пример можно выполнить целиком: временные наборы данных создаются внутри запроса через
WITH, отдельные таблицы не нужны.Этот запрос вернёт ноль строк.
Подзапрос возвращает значения
1иNULL. Для Бориса проверку можно представить так:Первое сравнение истинно. Результат второго неизвестен.
Что означает NULL
NULLобозначает отсутствие известного значения. Это не ноль, не пустая строка и не отдельное значение, которое можно сравнить обычным равенством.Например:
Первые два выражения дают
NULL, последнее —TRUE. Для проверки отсутствующего значения используютсяIS NULLиIS NOT NULL. Документация PostgreSQL о сравнениях.Условие
WHEREсохраняет только строки, для которых выражение истинно. Неизвестный результат для этого не подходит.Поэтому Борис и Вера исчезают из результата
NOT IN. Анна тоже не проходит условие: её идентификатор найден в сессиях.Решение через NOT EXISTS
Для нашей задачи удобно проверить отсутствие связанной строки:
Теперь результат соответствует условию:
id
name
2
Борис
3
Вера
Подзапрос проверяет наличие сессии для конкретного пользователя. Если совпадающих строк нет,
NOT EXISTSвозвращает истину.Сессия с
user_id = NULLне совпадает ни с одним из наших идентификаторов и не мешает проверке остальных пользователей. ПоведениеEXISTSи особенностиNOT INописаны в документации PostgreSQL о подзапросах.Зачем писать SELECT 1
В этой проверке важен сам факт существования строки. Число
1не является идентификатором пользователя и не возвращается в итоговый результат.Это распространённый способ записи проверки существования.
Можно ли оставить NOT IN
Да. В нашем примере достаточно исключить отсутствующие идентификаторы из подзапроса:
Этот фрагмент рассчитан на уже существующие таблицы
usersиsessions. Для запуска на демонстрационных данных используйте тот же блокWITH, что и выше.Здесь
users.idвсегда заполнен. Если сравниваемое поле внешнего запроса тоже допускаетNULL, отдельно определите, должны ли такие строки попадать в результат.Не стоит заменять
NULLслучайным числом черезCOALESCE: выбранное число может оказаться реальным идентификатором.Как объяснить решение на собеседовании
Хорошее объяснение включает три пункта:
Нужно найти пользователей, для которых отсутствует связанная сессия.
NOT INтребует внимания кNULLв сравниваемых данных.NOT EXISTSпозволяет явно записать проверку отсутствия связи.Утверждать, что
NOT EXISTSвсегда быстрее, не нужно. Производительность сравнивают на конкретных данных и планах выполнения.Проверьте себя
Измените демонстрационные данные:
добавьте ещё одну сессию Анны;
добавьте сессию Бориса;
оставьте строку с
user_id = NULL.Кто останется в результате запроса с
NOT EXISTS?Ответ: только Вера. Повторная сессия Анны не меняет результат проверки существования, а у Бориса теперь есть связанная запись.
Главное
Когда задача звучит как «найти записи, для которых нет связанных записей», рассмотрите
NOT EXISTS.Если используете
NOT IN, проверьте, могут ли сравниваемые значения содержатьNULL. Небольшой пример с отсутствующим значением помогает заметить ошибку до работы с настоящими данными.Хотите потренироваться в решении подобных задач? Посмотрите подготовку к собеседованиям по SQL. Если подзапросы пока вызывают затруднения, начните с практикума SQL с нуля.