Статья SQL // 11 мин чтения

Типы JOIN в SQL

INNER, LEFT, RIGHT, FULL и CROSS — на одном примере из двух маленьких таблиц. Переключай тип JOIN и смотри, какие строки сошлись, какие ушли в NULL, а какие пропали из выборки. Плюс разбор того, почему круги Эйлера врут, и пять ошибок, из-за которых цифры в отчёте перестают сходиться.

Все типы JOIN одной таблицей

Если нужен только ответ — вот он. «Левая» таблица та, что стоит до слова JOIN, «правая» — после.

ТипЧто оставляетКогда применять
INNER JOIN Только строки, у которых есть пара в обеих таблицах Нужны только совпадения. Например, заказы с известным пользователем
LEFT JOIN Все строки левой таблицы; правая часть — NULL, если пары нет Рабочая лошадка аналитика: сохранить всех пользователей, даже без заказов
RIGHT JOIN То же зеркально: все строки правой таблицы Почти не используют — переписывается в LEFT перестановкой таблиц
FULL OUTER JOIN Все строки обеих таблиц, несовпавшие — с NULL Сверка двух источников: что есть тут, чего нет там, и наоборот
CROSS JOIN Каждая строка с каждой, без условия Сетка комбинаций: все даты × все города. Или ошибка, если условие забыли
// Два сокращения
JOIN без уточнения — это INNER JOIN. А LEFT JOIN — сокращение от LEFT OUTER JOIN. Слова INNER и OUTER необязательны, и в реальном коде их обычно не пишут.

JOIN не склеивает таблицы, а ищет пары

Главное недопонимание темы — представлять JOIN как приклеивание одной таблицы к другой сбоку. Из этой картинки следует ложный вывод, что строк в результате останется столько же, сколько было слева. На практике их может стать и меньше, и больше.

Точнее так: СУБД перебирает строки левой таблицы и для каждой ищет все подходящие строки правой — те, где выполняется условие после ON. Дальше начинается арифметика:

Пара нашлась одна. В результат уходит одна строка — привычный случай.
Пар нашлось несколько. Строка левой таблицы попадёт в результат столько раз, сколько нашлось пар.
Пары нет. При INNER строка исчезнет, при LEFT — останется с NULL справа.
Условия нет вовсе. Парой считается каждая строка — это CROSS JOIN.

Тип JOIN отвечает ровно на один вопрос: что делать со строками, которым пары не нашлось. Совпавшие строки во всех типах ведут себя одинаково.

Визуализатор: посмотри, что происходит со строками

Две таблицы подобраны так, чтобы за один раз показать все случаи. У Ани два заказа — она даст две строки. У Веры и Глеба заказов нет — их видно только при LEFT и FULL. А заказ 104 оформлен на user_id = 5, которого нет в таблице пользователей, — он появится только при RIGHT и FULL.

// Визуализатор JOIN
users JOIN orders ON users.id = orders.user_id
users — левая
idname
orders — правая
iduser_idsum
Результат
3 строки
u.idu.nameo.ido.sum
// На что посмотреть
Переключи на LEFT: пользователей четверо, а строк в результате пять. Лишняя строка — из-за второго заказа Ани. Ни одна диаграмма с кругами этого не покажет, а в отчёте это ровно тот момент, где выручка начинает задваиваться.

Каждый тип по отдельности

INNER JOIN — только совпадения

Оставляет строки, для которых пара нашлась с обеих сторон. Всё остальное отбрасывается молча — и это его главная опасность.

INNER JOIN
SQL
SELECT u.name, o.id, o.sum
FROM users u
INNER JOIN orders o ON u.id = o.user_id;
-- 3 строки: Аня×2, Борис×1
-- Вера и Глеб исчезли: у них нет заказов
// Тихая потеря данных
Считаешь средний чек по пользователям через INNER JOIN — получаешь среднее только по покупателям, а не по всем пользователям. Цифра выйдет завышенной, и ошибка ничем себя не выдаст: запрос отработает без предупреждений.

LEFT JOIN — сохранить всех слева

Самый частый JOIN в аналитике. Берёт все строки левой таблицы, а тем, кому пары не нашлось, подставляет NULL в колонки правой.

LEFT JOIN
SQL
SELECT u.name, o.id, o.sum
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
-- 5 строк: Аня×2, Борис×1, Вера→NULL, Глеб→NULL

Отсюда же берётся приём «найти тех, у кого ничего нет» — антиджойн. Делаем LEFT JOIN и оставляем строки, где правая часть оказалась пустой:

Пользователи без заказов
SQL
SELECT u.name
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;
-- Вера, Глеб

RIGHT JOIN — то же самое наоборот

Зеркало LEFT: сохраняет все строки правой таблицы. Новых возможностей не даёт — любой RIGHT JOIN превращается в LEFT перестановкой таблиц местами. На практике его почти не пишут: запрос читается легче, когда главная таблица стоит первой и все соединения идут в одну сторону. Знать стоит, чтобы разбирать чужой код и не растеряться на собеседовании.

FULL OUTER JOIN — всё со всех сторон

Оставляет все строки обеих таблиц. Рабочий сценарий — сверка двух источников, когда важны расхождения в обе стороны: что есть в CRM и нет в биллинге, и наоборот.

// Диалекты
В MySQL FULL OUTER JOIN не поддерживается — его собирают из двух LEFT JOIN через UNION. В PostgreSQL, ClickHouse и большинстве аналитических хранилищ он есть.

CROSS JOIN — каждая с каждой

Соединяет без условия: каждая строка левой таблицы с каждой строкой правой. На четырёх и четырёх строках получается 16 — переключи визуализатор и посмотри.

Осмысленное применение одно: построить полную сетку комбинаций. Например, все даты месяца × все города, чтобы потом подтянуть к ней продажи и увидеть дни с нулём вместо пропущенных строк.

Почему круги Эйлера врут

Диаграммы с двумя пересекающимися кругами — самая тиражируемая картинка про JOIN. Она удобна и в одном месте прямо ошибочна.

Круги описывают операции над множествами: элемент либо есть, либо нет. JOIN работает не с множествами, а с таблицами, где строки могут повторяться. И вся разница видна на нашем примере: пользователей 4, а LEFT JOIN даёт 5 строк.

// Чего не покажет ни один круг
Строк на выходе может быть больше, чем в обеих таблицах
4 пользователя LEFT JOIN 4 заказа = 5 строк
У Ани два заказа — значит, Аня попадёт в результат дважды. В картинке с пересечением кругов «пересечение» — это одна область, и удвоиться в ней нечему.

Практический вывод: перед JOIN проверяй, уникален ли ключ в правой таблице. Если да — число строк не вырастет, и круги как грубая аналогия сойдут. Если нет — считай, сколько пар придётся на строку, иначе получишь задвоенные суммы.

Проверка уникальности ключа до JOIN
SQL
SELECT user_id, COUNT(*) AS cnt
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 1;
-- вернулись строки → ключ не уникален → JOIN размножит строки

5 ошибок, которые ломают цифры в отчётах

1. Условие на правую таблицу в WHERE вместо ON

Самая коварная. ON работает до соединения и решает, что считать парой. WHERE работает после и фильтрует готовый результат — вместе со строками, где правая часть равна NULL. LEFT JOIN молча становится INNER.

Одно и то же условие в двух местах
SQL
-- ❌ Вера и Глеб исчезнут: у них o.sum = NULL, а NULL > 500 → не проходит
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.sum > 500;

-- ✅ Все пользователи на месте, но подтянутся только заказы дороже 500
FROM users u
LEFT JOIN orders o ON u.id = o.user_id AND o.sum > 500;
2. JOIN по неуникальному ключу. Строки размножаются, суммы задваиваются. Проверяй ключ запросом выше.
3. COUNT(*) после LEFT JOIN. Считает строки, а не сущности: у пользователя без заказов выйдет 1, а не 0. Нужен COUNT(o.id).
4. Забытое условие ON. Получается CROSS JOIN. На больших таблицах запрос не столько соврёт, сколько не закончится.
5. JOIN по полю с разными типами. Строковый id против числового: часть баз приведёт молча, часть упадёт, а часть не сматчит ничего.
// Привычка, которая экономит часы
После каждого JOIN сравнивай число строк с исходным. Одна строчка SELECT COUNT(*) до и после ловит и потерю данных от INNER, и размножение от дублей — обе ошибки, которые иначе всплывут уже в отчёте у заказчика.

Проверь себя

Все задачи — на тех же двух таблицах из визуализатора. Сначала ответь, потом сверься.

1
Сколько строк вернёт users INNER JOIN orders ON u.id = o.user_id?
Показать ответ
3 строки. Аня даёт две (у неё заказы 101 и 102), Борис одну (заказ 103). Вера и Глеб отпадают — заказов нет. Заказ 104 тоже отпадает: пользователя с id = 5 не существует.
2
Тот же запрос, но LEFT JOIN. Сколько строк и почему это число больше числа пользователей?
Показать ответ
5 строк при четырёх пользователях. К трём совпавшим добавились Вера и Глеб с NULL, а Аня по-прежнему занимает две строки — по одной на каждый заказ. Это и есть размножение строк по неуникальному ключу.
3
Нужны все пользователи и сумма их заказов; у кого заказов нет — ноль. Что не так с запросом SELECT u.name, SUM(o.sum) FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE o.sum > 0 GROUP BY u.name?
Показать ответ
Условие WHERE o.sum > 0 отбрасывает строки Веры и Глеба, где o.sum равно NULL, — LEFT JOIN превращается в INNER. Условие нужно перенести в ON. Плюс SUM по пустому набору вернёт NULL, а не ноль, поэтому пишут COALESCE(SUM(o.sum), 0).
4
Сколько строк вернёт FULL OUTER JOIN и какие строки будут содержать NULL?
Показать ответ
6 строк. Три совпавшие (Аня×2, Борис), плюс Вера и Глеб с NULL в колонках заказа, плюс заказ 104 с NULL в колонках пользователя. FULL OUTER — единственный тип, где NULL появляется с обеих сторон.

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

Чем INNER JOIN отличается от LEFT JOIN?
INNER JOIN оставляет только строки, для которых нашлась пара в обеих таблицах. LEFT JOIN оставляет все строки левой таблицы, а тем, кому пары не нашлось, подставляет NULL справа. Практическое следствие: после INNER JOIN пользователи без заказов из выборки исчезнут, после LEFT JOIN останутся с NULL в сумме.
Какой JOIN используется по умолчанию, если написать просто JOIN?
Просто JOIN — это INNER JOIN. Слово INNER необязательно, и в PostgreSQL, MySQL и ClickHouse обе записи означают одно и то же. Аналогично LEFT JOIN — сокращение от LEFT OUTER JOIN.
Почему после JOIN строк стало больше, чем в исходной таблице?
Ключ соединения не уникален в правой таблице. Если у пользователя два заказа, он даст две строки результата. Это самая частая причина задвоенной выручки: сумма по колонке из левой таблицы после такого JOIN считается несколько раз. Проверяйте уникальность ключа GROUP BY … HAVING COUNT(*) > 1 до того, как соединять.
Чем отличается условие в ON от условия в WHERE при LEFT JOIN?
ON работает до соединения и определяет, какие строки считать парой. WHERE работает после и фильтрует готовый результат. При LEFT JOIN это критично: условие на правую таблицу в WHERE отбросит строки с NULL, и LEFT JOIN незаметно превратится в INNER. Нужно ограничить правую таблицу, сохранив все строки левой, — условие ставят в ON.
Что такое FULL OUTER JOIN и когда он нужен?
Оставляет все строки обеих таблиц: совпавшие соединяет, несовпавшим с обеих сторон подставляет NULL. На практике нужен при сверке двух источников, когда важны расхождения в обе стороны. В MySQL не поддерживается — эмулируют через UNION двух LEFT JOIN.
Нужен ли RIGHT JOIN вообще?
Технически он не добавляет возможностей: любой RIGHT JOIN переписывается в LEFT перестановкой таблиц. На практике его почти не используют — запрос читается проще, когда главная таблица первая и все соединения идут в одну сторону. Знать стоит для чужого кода и собеседований.

Связанные материалы

Главное про JOIN

JOIN не приклеивает таблицы, а ищет пары строк по условию. Тип JOIN отвечает только на один вопрос: что делать с теми, кому пары не нашлось. Совпавшие строки во всех типах ведут себя одинаково.

LEFT JOIN — рабочая лошадка аналитика: сохраняет всех слева и не теряет данные молча. INNER удобен, но выкидывает несовпавшее без предупреждения. FULL нужен для сверок, CROSS — для сеток комбинаций, RIGHT можно не писать никогда.

Практика: возьми любой свой запрос с JOIN и посчитай COUNT(*) до и после соединения. Если число выросло — ключ не уникален, и все суммы в этом отчёте нужно перепроверить.

АТ
Андрей Тарасенко
// Продуктовый аналитик · Авито · Ментор

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

Написать в Telegram
// ЗАКРЕПИ НА ПРАКТИКЕ

Напиши свой первый JOIN прямо в браузере

SQL-тренажёр на реальной базе маркетплейса: пишешь запрос, он выполняется по-настоящему и проверяется автоматически. 20 задач бесплатно, без регистрации на старте.

▶ Открыть SQL-тренажёр ★ 10 задач с собеса
Все материалы: База знаний · Telegram