SQL

NOT IN и NULL в SQL: почему запрос не возвращает строки

Разбираем на примере, почему NULL меняет результат NOT IN, как найти пользователей без сессий через NOT EXISTS и каких ошибок избежать

sqlka.ruОпубликовано: Чтение: 5 мин.

Нужно найти пользователей, которые ни разу не заходили в приложение. Вы пишете запрос с NOT IN, запускаете его и получаете пустой результат. Хотя пользователи без сессий точно есть.

Причиной может оказаться единственный NULL в подзапросе. Разберём ситуацию на небольшом примере.

Задача: найти пользователей без сессий

Есть две таблицы.

Пользователи — users:

id

name

1

Анна

2

Борис

3

Вера

Сессии — sessions:

id

user_id

101

1

102

NULL

У Анны есть сессия. Борис и Вера ещё не заходили. Вторая сессия не связана с зарегистрированным пользователем: например, это анонимное посещение.

Ожидаемый результат:

id

name

2

Борис

3

Вера

Запрос, который выглядит правильным

В 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;

Теперь результат соответствует условию:

id

name

2

Борис

3

Вера

Подзапрос проверяет наличие сессии для конкретного пользователя. Если совпадающих строк нет, 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: выбранное число может оказаться реальным идентификатором.

Как объяснить решение на собеседовании

Хорошее объяснение включает три пункта:

  1. Нужно найти пользователей, для которых отсутствует связанная сессия.

  2. NOT IN требует внимания к NULL в сравниваемых данных.

  3. NOT EXISTS позволяет явно записать проверку отсутствия связи.

Утверждать, что NOT EXISTS всегда быстрее, не нужно. Производительность сравнивают на конкретных данных и планах выполнения.

Проверьте себя

Измените демонстрационные данные:

  • добавьте ещё одну сессию Анны;

  • добавьте сессию Бориса;

  • оставьте строку с user_id = NULL.

Кто останется в результате запроса с NOT EXISTS?

Ответ: только Вера. Повторная сессия Анны не меняет результат проверки существования, а у Бориса теперь есть связанная запись.

Главное

Когда задача звучит как «найти записи, для которых нет связанных записей», рассмотрите NOT EXISTS.

Если используете NOT IN, проверьте, могут ли сравниваемые значения содержать NULL. Небольшой пример с отсутствующим значением помогает заметить ошибку до работы с настоящими данными.

Хотите потренироваться в решении подобных задач? Посмотрите подготовку к собеседованиям по SQL. Если подзапросы пока вызывают затруднения, начните с практикума SQL с нуля.

Закрепите SQL на практике

В SQLka можно изучать SQL последовательно или готовиться к собеседованиям. Запросы выполняются прямо в браузере — устанавливать базу данных не нужно.

sqlka.ru

support@sqlka.ru

← Все статьи