SQL JOIN простыми словами: INNER, LEFT, RIGHT и FULL с примерами
Автор: Казачкин Даниил Михайлович · Обновлено
JOIN объединяет строки двух источников по логическому условию. Главное различие видов соединения — не синтаксис, а судьба строк, для которых пара не нашлась: INNER их…
JOIN объединяет строки двух источников по логическому условию. Главное различие видов соединения — не синтаксис, а судьба строк, для которых пара не нашлась: INNER их отбрасывает, LEFT сохраняет слева, RIGHT — справа, а FULL сохраняет с обеих сторон. Ниже одни и те же данные последовательно показывают все четыре варианта и позволяют проверить результат вручную.
Исходные таблицы и ожидаемый результат
Пусть в магазине есть клиенты и заказы. У Анны два заказа, у Бориса ни одного, а один импортированный заказ пока не связан с клиентом. Последняя ситуация специально добавлена, чтобы разница между LEFT и FULL была видна на данных.
CREATE TABLE customers (
id bigint PRIMARY KEY,
name text NOT NULL
);
CREATE TABLE orders (
id bigint PRIMARY KEY,
customer_id bigint,
total numeric(12, 2) NOT NULL
);
INSERT INTO customers VALUES (1, 'Анна'), (2, 'Борис'), (3, 'Вера');
INSERT INTO orders VALUES
(101, 1, 1200.00),
(102, 1, 800.00),
(103, 3, 500.00),
(104, NULL, 300.00);В рабочей схеме для известных связей обычно добавляют внешний ключ. Здесь NULL оставлен допустимым ради сценария «заказ ещё не сопоставлен». Условие соединения будет одинаковым: customers.id = orders.customer_id.
INNER JOIN: только найденные пары
Внутреннее соединение возвращает комбинации строк, для которых условие ON истинно. У Анны появятся две строки, потому что одной строке клиента соответствуют два заказа. Борис исчезнет, а заказ 104 не попадёт в результат.
SELECT c.id, c.name, o.id AS order_id, o.total
FROM customers AS c
INNER JOIN orders AS o ON o.customer_id = c.id
ORDER BY c.id, o.id;Слово INNER разрешено опустить: JOIN без уточнения означает внутреннее соединение. Важно помнить о кратности. JOIN не «приклеивает один объект», а строит все подходящие пары; связь один-ко-многим поэтому размножает строку стороны «один».
LEFT JOIN: все строки слева
LEFT JOIN сначала формирует найденные пары, затем добавляет каждого клиента без заказа. Поля правой таблицы для такой строки получают NULL. Это подходящий вариант для отчёта «все клиенты и, если есть, их заказы».
SELECT c.id, c.name, o.id AS order_id, o.total
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
ORDER BY c.id, o.id;Чтобы найти клиентов без заказов, после соединения проверяют отсутствующий ключ правой стороны: WHERE o.id IS NULL. Лучше проверять гарантированно непустой первичный ключ, а не произвольный столбец, в котором NULL мог храниться и у реального заказа.
RIGHT JOIN и эквивалентный LEFT JOIN
RIGHT JOIN сохраняет все строки правого источника. Поэтому в выборке останется заказ 104, а колонки клиента будут NULL. Почти любой RIGHT JOIN можно переписать как более привычный LEFT JOIN, поменяв источники местами.
SELECT c.name, o.id AS order_id, o.total
FROM customers AS c
RIGHT JOIN orders AS o ON o.customer_id = c.id;
-- Тот же смысл с направлением чтения слева направо:
SELECT c.name, o.id AS order_id, o.total
FROM orders AS o
LEFT JOIN customers AS c ON c.id = o.customer_id;Единого правила «RIGHT JOIN плох» нет. Выбирайте запись, в которой базовая сущность запроса заметна сразу и которую команде проще проверить при ревью.
FULL JOIN: несовпадения с обеих сторон
FULL JOIN объединяет поведение LEFT и RIGHT: возвращает найденные пары, клиентов без заказов и заказы без клиента. Он полезен для сверки двух наборов, миграций и поиска расхождений.
SELECT c.id AS customer_id, c.name, o.id AS order_id, o.total
FROM customers AS c
FULL JOIN orders AS o ON o.customer_id = c.id
ORDER BY customer_id NULLS LAST, order_id;Для обычного экрана приложения FULL JOIN нужен реже, чем INNER или LEFT. Его наличие в запросе должно следовать из требования сохранить обе группы несовпавших строк, а не из желания «показать вообще всё».
Проверка результата, CROSS JOIN и подсчёт заказов
На исходных данных INNER возвращает 3 строки, LEFT — 4, RIGHT — 4, FULL — 5. Эти числа относятся к парам строк, а не к количеству уникальных клиентов. Для проверки результата полезнее заранее выписать пары ключей, чем добавлять DISTINCT после неожиданного результата.
CROSS JOIN формирует каждую комбинацию без условия связи: три клиента и четыре заказа дают 12 строк. Такое произведение подходит, например, для сетки «каждый товар × каждый день», но не заменяет соединение заказа с его владельцем. Self join означает соединение таблицы с самой собой под разными алиасами; к нему применяются те же виды JOIN.
SELECT COUNT(*) AS combinations
FROM customers CROSS JOIN orders;
-- 12
SELECT c.id, c.name,
COUNT(*) AS joined_rows,
COUNT(o.id) AS order_count,
COALESCE(SUM(o.total), 0) AS total
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
GROUP BY c.id, c.name
ORDER BY c.id;| Клиент | joined_rows | order_count | total |
|---|---|---|---|
| Анна | 2 | 2 | 2000.00 |
| Борис | 1 | 0 | 0 |
| Вера | 1 | 1 | 500.00 |
У Бориса COUNT(*) учитывает сохранённую внешним соединением строку, хотя заказа нет. COUNT(o.id) пропускает NULL и правильно даёт ноль заказов. SUM по отсутствующим заказам тоже даёт NULL; COALESCE заменяет его нулём для отчёта. Это разные операции, поэтому подменять одну другой нельзя.
Почему фильтр в WHERE меняет внешний JOIN
Условия ON определяют, какие строки считаются парой. Условия WHERE фильтруют уже получившийся результат. Поэтому перенос проверки правой таблицы из ON в WHERE может незаметно удалить строки с NULL и превратить LEFT JOIN по смыслу в INNER JOIN.
-- Сохраняет всех клиентов, но присоединяет только крупные заказы:
SELECT c.name, o.id, o.total
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.id AND o.total >= 1000;
-- Оставляет только клиентов, у которых нашёлся крупный заказ:
SELECT c.name, o.id, o.total
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
WHERE o.total >= 1000;Оба запроса корректны, но отвечают на разные вопросы. Перед написанием SQL сформулируйте, какие строки обязаны сохраниться даже без соответствия.
ON, USING и неоднозначные имена
USING (customer_id) короче, когда столбец связи имеет одинаковое имя с обеих сторон, и выводит одну общую колонку вместо двух. ON гибче: он позволяет связывать столбцы с разными именами и добавлять условия. NATURAL JOIN в прикладном коде рискован: набор общих имён может измениться после добавления столбца, хотя текст запроса останется прежним.
Квалифицируйте повторяющиеся имена алиасами: c.id, o.id. Не используйте SELECT * в стабильном API-контракте — явный список полей не меняется от добавления технической колонки.
Производительность без магии
Индекс часто нужен на столбце, по которому ищутся соответствия, например orders(customer_id), но его польза зависит от размера таблиц, селективности и полного запроса. Внешний ключ сам по себе выражает целостность, а решение о плане принимает PostgreSQL. Сначала измерьте реальный запрос через EXPLAIN, затем меняйте индекс и сравнивайте тот же сценарий.
Практика
- Выполните четыре соединения на тестовых данных и вручную предскажите число строк до запуска.
- Напишите запрос «все клиенты и сумма их заказов», сохранив клиентов без покупок. Используйте
COALESCE(SUM(o.total), 0)и корректныйGROUP BY. - Найдите отдельно клиентов без заказов и заказы без клиента.
- Добавьте условие «учитывать только заказы от 1000» сначала в ON, затем в WHERE и объясните различие результатов.
Критерий готовности: вы можете назвать, какая сторона обязана сохраниться, предсказать появление NULL и объяснить размножение строк до выполнения запроса.
Что важно запомнить
Выбор вида JOIN начинается с вопроса о несовпавших строках. INNER оставляет только пары, LEFT и RIGHT сохраняют одну заданную сторону, FULL — обе. Условия ON управляют сопоставлением, WHERE — итоговой фильтрацией, а кратность данных определяет число строк результата.
Частые вопросы
Чем JOIN отличается от UNION?
JOIN расширяет строку колонками другого источника по условию связи. UNION складывает совместимые результаты вертикально и добавляет строки, а не колонки.
Почему после JOIN строк стало больше, чем в исходной таблице?
Одна строка могла найти несколько соответствий. Проверьте реальную кардинальность связи и уникальность ключа второй стороны, а не маскируйте ошибку случайным DISTINCT.
Как получить только записи без соответствия?
Используйте LEFT JOIN и проверку IS NULL по непустому ключу правой стороны. Для симметричной сверки применяют FULL JOIN и проверяют ключи обеих сторон.
Всегда ли нужен индекс на колонке JOIN?
Нет. На маленьких таблицах последовательный просмотр может быть дешевле, а индекс имеет стоимость хранения и записи. Решение подтверждают планом и измерениями.
Связанные исследования
- B-tree и планировщик PostgreSQL: почему индекс не обязан ускорять запрос — Связываем устройство B-tree с cost-based planner PostgreSQL: селективность, составные индексы, EXPLAIN ANALYZE, статистика и пределы index-only scan.
- PostgreSQL: почему снимок данных не гарантирует согласованность решения — MVCC, аномалия write skew и Serializable Snapshot Isolation: разбор двух транзакций, инварианта и правильной границы повтора.