Раздел 10 · Databases

LEFT JOIN: условие в WHERE против ON

junior~15 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Этот вопрос задают, чтобы отличить человека, который выучил определения, от человека, который писал отчёты. Два запроса отличаются одним перенесённым условием, а результаты у них разные. В прошлом уроке мы разобрали виды соединений, теперь посмотрим, где LEFT JOIN ломается незаметно. Все запросы идут на учебной базе блока.

Как спрашивают

Нужен список всех клиентов и их январских заказов. Чем отличаются эти два запроса?

-- вариант А
SELECT c.name, o.id
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.created_at < '2026-02-01';

-- вариант Б
SELECT c.name, o.id
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id AND o.created_at < '2026-02-01';

Короткий ответ

Условие в ON участвует в подборе пары: клиент без подходящего заказа всё равно остаётся в результате с NULL. Условие в WHERE применяется после соединения и отбрасывает строки, где колонки правой таблицы равны NULL. Поэтому условие на правую таблицу в WHERE превращает LEFT JOIN во внутреннее соединение: клиенты без январских заказов исчезают.

Разбор: фейсконтроль у входа и в зале

Представь вечеринку, куда каждый гость приходит со спутником или один. Есть правило: «спутник должен быть старше 18».

  • Условие в ON это фейсконтроль у двери для спутников. Несовершеннолетнего спутника не пускают, а гость проходит один. В зале все гости.
  • Условие в WHERE это проверка уже в зале: охранник обходит пары и выводит каждую, где спутник не подходит. Гость, пришедший один, тоже не проходит проверку «спутник старше 18», ведь спутника нет. Его выводят вместе с остальными.

Запрос выполняется так же, как в уроке про порядок выполнения: сначала FROM и JOIN собирают пары, и только потом WHERE фильтрует готовые строки.

Вариант Б: условие в ON

SELECT c.name, o.id AS order_id, o.amount
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id AND o.created_at < '2026-02-01'
ORDER BY c.id, o.id;
name  | order_id | amount
------+----------+-------
Ольга |        1 |   5000
Ольга |        2 |   3000
Павел |     NULL |   NULL
Роман |     NULL |   NULL
Света |     NULL |   NULL

Все четыре клиента на месте. У Павла и Романа заказы есть, но не январские, поэтому пары не нашлось. Света не заказывала вообще.

Вариант А: условие в WHERE

SELECT c.name, o.id AS order_id, o.amount
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.created_at < '2026-02-01'
ORDER BY c.id, o.id;
name  | order_id | amount
------+----------+-------
Ольга |        1 |   5000
Ольга |        2 |   3000

Осталась одна Ольга. Сначала LEFT JOIN честно сохранил Свету с NULL, а потом WHERE проверил NULL < '2026-02-01'. Сравнение с NULL даёт неизвестность, а WHERE пропускает только TRUE, как в уроке про трёхзначную логику. Строки Павла и Романа с февральскими и мартовскими заказами отсеялись по дате.

Результат совпадает с обычным INNER JOIN с тем же условием. PostgreSQL это замечает сам: в EXPLAIN варианта А стоит узел Hash Join, а не Hash Left Join. Оптимизатор видит, что строки с NULL справа всё равно отфильтруются, и выполняет внутреннее соединение.

Где условие в WHERE правильное

Условие на левую таблицу можно смело писать в WHERE:

SELECT c.name, o.id AS order_id
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE c.city = 'Казань'
ORDER BY c.id, o.id;

Это фильтр «какие клиенты нам нужны», и он работает как ожидается: Ольга с тремя заказами и Роман с одним. А вот то же условие в ON дало бы странный результат: все четыре клиента остались бы, но заказы подтянулись бы только казанцам. Правило простое для запоминания:

Условие наВ ONВ WHERE
правую таблицуфильтрует пары, левые строки остаютсяубивает LEFT JOIN
левую таблицулевые строки остаются, у лишних NULL справафильтрует левые строки

дальше после входа

Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.