Этот вопрос задают, чтобы отличить человека, который выучил определения, от человека, который писал отчёты. Два запроса отличаются одним перенесённым условием, а результаты у них разные. В прошлом уроке мы разобрали виды соединений, теперь посмотрим, где 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.