Ловушка WHERE при внешнем объединении
Узнайте, почему фильтрация столбца внешнего объединения в WHERE незаметно превращает его во внутреннее объединение.
«Ловушка WHERE при внешнем объединении» — бесплатный урок SQL Interview Prep на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения SQL Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс SQL Interview Prep содержит 4 уроков всего.
Ловушка, на которой ошибаются все
Это самая распространённая ошибка во внешних соединениях, которую интервьюеры намеренно провоцируют: «Покажите всех клиентов и их заказы за 2024 год, включая клиентов без заказов за 2024 год».
Кандидат пишет LEFT JOIN, а затем добавляет фильтр по дате в WHERE, и клиенты без заказов за 2024 год незаметно исчезают. LEFT JOIN тихо превращается в INNER JOIN. Понимание причины — признак уровня опытного специалиста.
Ошибочный запрос
Вот ошибка. Запрос выглядит разумно: сохранить всех клиентов, соединить их с заказами и отфильтровать заказы за 2024 год.
Но клиенты без заказов или без заказов за 2024 год исчезают из результата. Требование включить их нарушено.
-- BUG: drops customers with no 2024 order
SELECT c.name, o.id, o.order_date
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.order_date >= '2024-01-01';Почему это не работает
Вспомните порядок выполнения: сначала происходит JOIN, создавая строки, в которых у несовпавших клиентов каждый столбец заказа содержит NULL. Затем выполняется WHERE.
Для несовпавшего клиента o.order_date равен NULL, поэтому выражение o.order_date >= '2024-01-01' вычисляется как UNKNOWN, а не как истина. WHERE сохраняет только истинные строки, поэтому строки с NULL отфильтровываются — именно те строки, которые LEFT JOIN должен был сохранить.
NULL нарушает работу фильтра
Любое сравнение с NULL даёт UNKNOWN: NULL >= '2024-01-01' — UNKNOWN, NULL = 5 — UNKNOWN, и даже NULL <> 5 — UNKNOWN.
Поскольку WHERE пропускает только строки, вычисляющиеся как TRUE, каждая сохранённая строка без соответствия отбрасывается. Вся цель внешнего соединения сводится на нет одним предикатом WHERE по столбцу правой таблицы.
Исправление: фильтр в ON
Переместите фильтр в предложение ON. Там он становится частью условия сопоставления и применяется до сохранения строк, поэтому несовпавшие клиенты по-прежнему сохраняются с NULL.
-- CORRECT: filter lives in ON
SELECT c.name, o.id, o.order_date
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.id
AND o.order_date >= '2024-01-01';
-- customers with no 2024 order: kept, NULL orderON и WHERE в одном предложении
Правило, которое стоит произнести на собеседовании:
Для сохраняемой (внешней) таблицы условия по другой таблице должны находиться в ON, а условия по самой сохраняемой таблице — в WHERE.
ONопределяет, что считается совпадением (выполняется во время соединения).WHEREфильтрует итоговые строки (выполняется после соединения и удаляет строки с NULL).
Результаты бок о бок
Одни и те же данные, два места для фильтра, разные ответы. Предположим, у Carol нет заказа за 2024 год.
- Фильтр в WHERE: Carol исчезает. Фактически это внутреннее соединение.
- Фильтр в ON: Carol появляется один раз со значениями NULL в столбцах заказа, и требование выполнено.
Разница в результате — суть этой ловушки.
-- ON version output
-- Alice | 50 | 2024-03-01
-- Bob | 20 | 2024-05-02
-- Carol | NULL | NULL <-- preservedКогда WHERE действительно уместен
Не всякий WHERE во внешнем соединении является ошибкой. Фильтрация сохраняемой таблицы допустима: она не связана с NULL, появившимися при соединении.
Кроме того, антисоединение из предыдущего урока намеренно использует WHERE o.id IS NULL, чтобы воспользоваться именно этим поведением. Важно понимать, с каким случаем вы имеете дело.
-- Fine: filtering the preserved (left) table
SELECT c.name, o.id
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE c.country = 'US';Эвристика обнаружения
Проверяя внешнее соединение, просмотрите предложение WHERE в поисках предикатов по столбцам несохраняемой таблицы (кроме проверок IS NULL для антисоединения).
Если вы видите o.someColumn = ... или проверку диапазона либо равенства по внешней стороне в WHERE, заподозрите эту ловушку. Спросите себя: «Не превращает ли это мой LEFT JOIN в INNER JOIN?» Обычно превращает.
Несколько условий
Вы можете сочетать оба места размещения. Условия сопоставления по правой таблице помещайте в ON, а настоящее условие фильтрации после соединения по левой таблице — в WHERE. Они без проблем работают вместе.
SELECT c.name, o.id, o.amount
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.id
AND o.amount > 100 -- match condition
WHERE c.signup_year = 2023; -- preserved-table filterКак объяснить это вслух
На собеседовании объясняйте механизм, а не только исправление:
«Сначала выполняется соединение и заполняет правые столбцы несовпавших строк значениями NULL. Предикат WHERE по этим столбцам для строк с NULL вычисляется как UNKNOWN, а WHERE отбрасывает неистинные строки, поэтому внешнее соединение превращается во внутреннее. Размещение предиката в ON оставляет его условием сопоставления и сохраняет несовпавшие строки». Такое объяснение всегда производит впечатление.
Быстрая проверка
Вы должны перечислить всех клиентов и только их заказы за 2024 год, сохранив клиентов, у которых таких заказов не было.
Итоги
Фильтрация столбца несохраняемой таблицы в WHERE незаметно превращает внешнее соединение во внутреннее, потому что NULL из несовпавших строк не проходит предикат (даёт UNKNOWN), и WHERE отбрасывает такие строки.
- Условия сопоставления с внешней таблицей помещайте в
ON. - Фильтры по сохраняемой таблице помещайте в
WHERE. IS NULLв WHERE — это намеренное антисоединение, а не ловушка.- Объясняйте порядок выполнения, чтобы показать понимание механизма.
Часто задаваемые вопросы
Урок «Ловушка WHERE при внешнем объединении» бесплатный?
Да — полный текст урока «Ловушка WHERE при внешнем объединении» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс SQL Interview Prep, подпишись на CoddyKit PRO. Курс SQL Interview Prep содержит 4 уроков всего.
Чему я научусь в уроке «Ловушка WHERE при внешнем объединении»?
Узнайте, почему фильтрация столбца внешнего объединения в WHERE незаметно превращает его во внутреннее объединение. Ты практикуешь SQL Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать SQL Interview Prep?
Предыдущий опыт не требуется. SQL Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Ловушка WHERE при внешнем объединении»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке SQL Interview Prep?
Да. Каждый урок SQL Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- LEFT JOIN и сохранение несовпавших строк
- Смысл RIGHT и FULL OUTER JOIN
- Поиск строк без совпадений (антиобъединение)
- Ловушка WHERE при внешнем объединении