Перейти к содержанию
Бэкенд и системы

LEFT JOIN и WHERE в SQL: куда пропадают строки

LEFT JOIN не гарантирует сохранение левых строк после любого WHERE: последующий фильтр может отклонить результат с NULL справа. На данных трёх учеников сравним фильтрацию совпадений и готовых строк, а затем разберём дубликаты и ошибку с COUNT(*).

Автор: Опубликовано

4 мин чтенияРедакционный разборОбновлено
  • SQL
  • PostgreSQL
  • LEFT JOIN
  • собеседование
Главная мысль

Если нужно сохранить каждую левую строку, условие допустимости правого совпадения помещайте в ON. Совпадения считайте через COUNT по ненулевому ключу справа, а отсутствие подходящих записей выражайте через NOT EXISTS.

Сначала определите результат и подготовьте данные

Требование: показать всех учеников и их успешные попытки, включая учеников без успехов. У Ada две успешные попытки, у Boris одна неуспешная, у Chen попыток нет. Эти случаи позволяют отличить неподходящее совпадение от полного отсутствия совпадений.

Запускайте подготовку и последующие запросы в одной сессии PostgreSQL: таблицы временные. Первичные ключи не допускают NULL. До написания запроса решите, нужна одна строка на попытку или на ученика — это разная детализация результата.

CREATE TEMP TABLE learners (
  id integer PRIMARY KEY,
  name text NOT NULL
);
CREATE TEMP TABLE attempts (
  id integer PRIMARY KEY,
  learner_id integer NOT NULL REFERENCES learners(id),
  status text NOT NULL
);
INSERT INTO learners VALUES (1, 'Ada'), (2, 'Boris'), (3, 'Chen');
INSERT INTO attempts VALUES
  (10, 1, 'passed'), (11, 1, 'passed'), (12, 2, 'failed');

Почему фильтр WHERE удаляет учеников

Этот запрос возвращает только попытки Ada с идентификаторами 10 и 11. У Boris есть присоединённая строка, но status равен failed. Для Chen правая сторона дополнена NULL, а NULL = 'passed' даёт неизвестный результат. WHERE сохраняет только истинные условия, поэтому оба ученика исчезают.

Для конкретно этого условия на правую сторону набор строк совпадает с INNER JOIN и таким фильтром. Это не означает, что любой WHERE превращает LEFT JOIN во внутреннее соединение: условие на левую таблицу или условие, принимающее NULL, может работать иначе.

SELECT l.name, a.id AS attempt_id
FROM learners l
LEFT JOIN attempts a ON a.learner_id = l.id
WHERE a.status = 'passed'
ORDER BY l.id, a.id;

Перенесите условие в ON для сохранения всех учеников

Теперь совпадениями считаются только успешные попытки. Неуспешная попытка Boris исключается на этапе сопоставления, поэтому ему достаётся строка с NULL справа, как и Chen. У Ada по-прежнему два совпадения: LEFT JOIN не обещает одну строку на левую сущность.

Добавление OR a.id IS NULL к прежнему WHERE не равнозначно этому исправлению. Оно вернёт Chen, у которого не было попыток, но не Boris: его неуспешная попытка уже совпала в исходном соединении. Такой контрпример хорошо проверяет предлагаемый ремонт запроса.

SELECT l.name, a.id AS attempt_id
FROM learners l
LEFT JOIN attempts a
  ON a.learner_id = l.id AND a.status = 'passed'
ORDER BY l.id, a.id;
Результат с условием status в ON
Ученикattempt_idПричина
Ada10Первое успешное совпадение
Ada11Второе успешное совпадение
BorisNULLНет успешного совпадения
ChenNULLВообще нет попыток

Считайте совпадения, а не сохранённую строку

Если нужна одна строка на ученика, агрегируйте на этом уровне. Запрос вернёт Ada 2, Boris 0, Chen 0. COUNT(a.id) пропускает NULL из дополненной строки. COUNT(*) посчитал бы саму сохранённую строку и ошибочно показал 1 для Boris и Chen.

Выбор a.id намеренный: для настоящей попытки этот первичный ключ не может быть NULL. Подсчёт по nullable-полю бизнеса мог бы пропустить реальные совпадения. Если позже добавится другая связь один-ко-многим, проверьте размножение строк перед тем, как считать новый результат количеством попыток.

SELECT l.name, COUNT(a.id) AS passed_attempts
FROM learners l
LEFT JOIN attempts a
  ON a.learner_id = l.id AND a.status = 'passed'
GROUP BY l.id, l.name
ORDER BY l.id;

Ищите отсутствие успехов через NOT EXISTS

Если нужны только ученики без успешных попыток, NOT EXISTS прямо выражает этот вопрос. Запрос возвращает Boris и Chen по одному разу. Его смысл остаётся тем же, если у Ada появятся новые успешные попытки: нам важен факт отсутствия подходящей записи.

Не утверждайте, что эта форма всегда быстрее антисоединения через LEFT JOIN. План зависит от индексов, статистики и данных. Сначала докажите эквивалентность результатов, затем сравнивайте планы на одинаковых параметрах и реалистичных объёмах.

SELECT l.name
FROM learners l
WHERE NOT EXISTS (
  SELECT 1 FROM attempts a
  WHERE a.learner_id = l.id AND a.status = 'passed'
)
ORDER BY l.id;

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

При изменении внешнего соединения проверяйте три случая: справа нет записи, есть только неподходящие записи и есть несколько подходящих. Ожидаемый результат запишите до выполнения запроса. Единственное успешное совпадение не обнаружит ни исчезновение строк, ни ошибку агрегации.

Объясняйте решение в порядке: детализация результата, условие сопоставления, фильтрация строк, агрегация. Для общей подготовки используйте сборник SQL, а для следующего требования — ранжирования попыток внутри ученика — практику оконных функций.

Коротко

Частые вопросы

WHERE после LEFT JOIN всегда ошибка?

Нет. WHERE нужен, если требуется фильтровать готовые строки соединения. Ошибка — отклонять NULL условием на правую сторону и одновременно ожидать сохранения всех левых строк.

Убирает ли LEFT JOIN дубликаты?

Нет. Каждое подходящее совпадение создаёт строку. До добавления DISTINCT определите, нужны ли сами совпадения, агрегированный результат или проверка существования.

Можно заменить NOT EXISTS на WHERE a.id IS NULL?

Да, антисоединение через LEFT JOIN подходит, если все условия допустимости находятся в ON, а проверяемый ключ не бывает NULL у реальных совпадений. NOT EXISTS выражает намерение без зависимости от выбранного ключа.

Источники

Источники и редакционная политика

Сведения проверены 21 октября 2026 г. Ссылки рядом с разделами указывают источники фактов и технических объяснений. Выводы, учебные сценарии и рекомендации по подготовке — редакционная работа RecallDeck.

От чтения к воспроизведению

Отрепетируйте полный цикл интервью.

RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.

Начать подготовку

Продолжить подготовку