JOIN в SQL соединяет строки двух таблиц по условию: вы пишете FROM таблица1 JOIN таблица2 ON таблица1.ключ = таблица2.ключ, и база склеивает каждую строку первой таблицы с подходящими строками второй. Просто JOIN означает INNER JOIN и оставляет только пары с совпадением. Если нужны все строки одной из таблиц, даже без пары, берите LEFT JOIN.
Ниже разбираем все виды соединений на одних и тех же маленьких таблицах. Запросы работают в MySQL 8.0 и PostgreSQL 16, а там, где синтаксис расходится, это отмечено отдельно. Если вы только начинаете, сначала пригодится разбор команды SELECT от WHERE до GROUP BY, а общий обзор языка лежит в статье «Язык SQL: что это такое и зачем его учить».
Как устроен JOIN: синтаксис и учебные таблицы

Общая форма запроса одна для всех видов соединения:
SELECT столбцы
FROM левая_таблица AS a
[INNER | LEFT | RIGHT | FULL] JOIN правая_таблица AS b
ON a.ключ = b.ключ;
Левая таблица стоит после FROM, правая после JOIN. Условие в ON обычно сравнивает внешний ключ одной таблицы с первичным ключом другой. Псевдонимы AS a и AS b необязательны, но без них длинные запросы читать тяжело.
В разговоре соединения называют джойнами или джоинами: «сделай джоин с заказами», «тут нужен левый джойн». Как использовать JOIN, проще всего понять на живых данных.
Для примеров возьмём три таблицы. В отделе «Склад» нет сотрудников, у Глеба не указан отдел, а у Анны два заказа:
CREATE TABLE departments (id INT PRIMARY KEY, name VARCHAR(50));
INSERT INTO departments VALUES (1, 'Продажи'), (2, 'Аналитика'), (3, 'Склад');
CREATE TABLE employees (id INT PRIMARY KEY, name VARCHAR(50), dept_id INT, manager_id INT);
INSERT INTO employees VALUES
(1, 'Анна', 1, NULL), (2, 'Борис', 1, 1), (3, 'Вера', 2, 1), (4, 'Глеб', NULL, 3);
CREATE TABLE orders (id INT PRIMARY KEY, employee_id INT, amount INT);
INSERT INTO orders VALUES (101, 1, 5000), (102, 1, 7000), (103, 3, 4000);
Как создать такие таблицы у себя, показано в инструкциях про создание базы данных в SQL и про таблицу в pgAdmin 4.
Виды соединений таблиц: шпаргалка

Если нужно быстро вспомнить, какой JOIN что возвращает, хватит этой таблицы. Подробности и вывод каждого запроса ниже.
| Вид | Что возвращает | Строк на наших данных |
|---|---|---|
INNER JOIN (или просто JOIN) |
только пары с совпадением | 3 |
LEFT JOIN |
все строки левой таблицы, справа NULL, если пары нет | 4 |
RIGHT JOIN |
все строки правой таблицы, слева NULL, если пары нет | 4 |
FULL OUTER JOIN |
все строки обеих таблиц | 5 |
CROSS JOIN |
каждую строку с каждой, без условия | 12 |
| SELF JOIN | таблицу, соединённую сама с собой (любым из видов выше) | зависит от вида |
Слова INNER и OUTER можно не писать: LEFT JOIN и LEFT OUTER JOIN означают одно и то же. В документации PostgreSQL это сказано прямо: «Слова INNER и OUTER необязательны во всех формах» (раздел о табличных выражениях).
INNER JOIN: только совпадения
SELECT e.name, d.name AS dept
FROM employees AS e
INNER JOIN departments AS d ON e.dept_id = d.id
ORDER BY e.id;
name | dept
-------+-----------
Анна | Продажи
Борис | Продажи
Вера | Аналитика
Глеб пропал, потому что у него нет отдела. «Склад» тоже пропал: в нём никто не работает. Внутреннее соединение молча отбрасывает строки без пары, и это главный источник вопросов «куда делись данные».
LEFT JOIN: все строки левой таблицы
SELECT e.name, d.name AS dept
FROM employees AS e
LEFT JOIN departments AS d ON e.dept_id = d.id
ORDER BY e.id;
name | dept
-------+-----------
Анна | Продажи
Борис | Продажи
Вера | Аналитика
Глеб | NULL
Глеб вернулся, а в столбце отдела у него NULL. Левое соединение подходит, когда главная таблица одна и терять её строки нельзя: все клиенты и их заказы, все товары и их продажи. Что делать с этими NULL дальше, разобрано в статье о NULL в SQL.
RIGHT JOIN: зеркало LEFT
SELECT e.name, d.name AS dept
FROM employees AS e
RIGHT JOIN departments AS d ON e.dept_id = d.id
ORDER BY d.id, e.id;
name | dept
-------+-----------
Анна | Продажи
Борис | Продажи
Вера | Аналитика
NULL | Склад
Теперь сохранились все отделы, а Глеб выпал. Тот же результат даст LEFT JOIN, если поменять таблицы местами. Документация MySQL советует так и делать: «To keep code portable across databases, it is recommended that you use LEFT JOIN instead of RIGHT JOIN» (справка по JOIN). Запрос, где все соединения левые, ещё и читать проще.
FULL OUTER JOIN: обе таблицы целиком
Полное внешнее соединение оставляет строки обеих таблиц: и Глеба без отдела, и отдел без сотрудников. В PostgreSQL, SQL Server и SQLite оно пишется одной строкой:
SELECT e.name, d.name AS dept
FROM employees AS e
FULL OUTER JOIN departments AS d ON e.dept_id = d.id
ORDER BY d.id NULLS LAST, e.id;
name | dept
-------+-----------
Анна | Продажи
Борис | Продажи
Вера | Аналитика
NULL | Склад
Глеб | NULL
В MySQL полного соединения нет. В грамматике JOIN из справки MySQL 8.0 есть только INNER, CROSS, LEFT и RIGHT, а запрос с FULL OUTER JOIN в MySQL 8.0 падает с синтаксической ошибкой 1064. В SQLite правое и полное соединение появились только в версии 3.39.0: «Add (long overdue) support for RIGHT and FULL OUTER JOIN» (журнал изменений SQLite).
Решение для MySQL: склеить левое и правое соединение через UNION.
SELECT e.name, d.name AS dept
FROM employees AS e LEFT JOIN departments AS d ON e.dept_id = d.id
UNION
SELECT e.name, d.name
FROM employees AS e RIGHT JOIN departments AS d ON e.dept_id = d.id;
Вывод тот же, пять строк. UNION убирает повторы так же, как DISTINCT, поэтому три общие строки не задвоятся. Если в данных бывают настоящие дубли, которые нужно сохранить, понадобится UNION ALL и условие WHERE e.id IS NULL во второй половине.
Если ваша основная база MySQL, в каталоге есть отдельная подборка курсов по MySQL.
CROSS JOIN: все комбинации
SELECT COUNT(*)
FROM employees
CROSS JOIN departments;
Результат: 12, то есть четыре сотрудника умножить на три отдела. Условия у перекрёстного соединения нет, оно выдаёт декартово произведение. Пригождается редко и по делу: сетка «каждый магазин × каждый день», все сочетания размеров и цветов товара. На больших таблицах результат растёт как произведение строк, поэтому случайный CROSS JOIN на рабочих данных может надолго загрузить сервер.
SELF JOIN: таблица сама с собой
Отдельного ключевого слова для него нет. Это обычный JOIN, где одна и та же таблица стоит дважды под разными псевдонимами. Классическая задача: вывести каждого сотрудника рядом с руководителем.
SELECT e.name, m.name AS manager
FROM employees AS e
LEFT JOIN employees AS m ON e.manager_id = m.id
ORDER BY e.id;
name | manager
-------+---------
Анна | NULL
Борис | Анна
Вера | Анна
Глеб | Вера
Здесь стоит LEFT, а не INNER: у Анны руководителя нет, и внутреннее соединение выбросило бы её из списка.
Как найти строки без пары

Частая задача на работе и на собеседовании: найти клиентов без заказов, товары без продаж, отделы без сотрудников. Такой приём называют анти-соединением, и делается он через LEFT JOIN с проверкой на NULL:
SELECT d.name
FROM departments AS d
LEFT JOIN employees AS e ON e.dept_id = d.id
WHERE e.id IS NULL;
name
-------
Склад
Проверять на NULL нужно столбец, который в правой таблице никогда не бывает пустым, лучше всего первичный ключ. Если проверить столбец, где NULL встречается в самих данных, в результат попадут лишние строки.
Тот же ответ даёт NOT EXISTS, и многим он читается понятнее:
SELECT d.name
FROM departments AS d
WHERE NOT EXISTS (SELECT 1 FROM employees AS e WHERE e.dept_id = d.id);
Такие задачи регулярно дают на собеседованиях, примеры с разбором собраны в статье о собеседовании на аналитика данных.
ON, USING и NATURAL: три способа задать условие
По документации PostgreSQL, «Условие соединения указывается в предложении ON или USING, либо неявно задаётся ключевым словом NATURAL». На практике это выглядит так:
ONпринимает любое условие: равенство ключей, несколько столбцов черезAND, даже диапазон дат. Подходит всегда.USING (столбец)подходит, когда ключ в обеих таблицах называется одинаково:JOIN orders USING (employee_id). В выводе такой столбец появится один раз, а не два.NATURAL JOINсоединяет по всем столбцам с одинаковыми именами. В справке PostgreSQL: «NATURAL — сокращённая форма USING: она образует список USING из всех имён столбцов, существующих в обеих входных таблицах».
С NATURAL легко ошибиться. В наших таблицах employees и departments совпадают два имени, id и name, и база будет искать строки, где совпадают оба:
SELECT COUNT(*)
FROM employees
NATURAL JOIN departments;
Ответ: 0. Добавите завтра в обе таблицы столбец created_at, и запрос молча поменяет смысл. Поэтому в рабочем коде пишите ON или USING.
Как объединить три и больше таблиц
Каждое следующее соединение просто дописывается ниже. База соединяет первые две таблицы, к результату цепляет третью и так далее:
SELECT e.name, d.name AS dept, o.amount
FROM employees AS e
JOIN departments AS d ON e.dept_id = d.id
JOIN orders AS o ON o.employee_id = e.id
ORDER BY o.id;
name | dept | amount
------+-----------+--------
Анна | Продажи | 5000
Анна | Продажи | 7000
Вера | Аналитика | 4000
Анна встречается дважды, потому что у неё два заказа. Так работает связь «один ко многим»: строка сотрудника повторяется столько раз, сколько у неё пар в таблице заказов.
Отчёт из трёх-пяти таблиц для SQL-аналитика обычная ежедневная задача, и почти каждая ошибка в нём связана с порядком соединений.
Если цепочка смешивает виды соединений, следите за порядком. Одно внутреннее соединение после левого может отрезать те самые строки, ради которых вы ставили LEFT.
Условие в ON или в WHERE
Для INNER JOIN разницы нет.
В справке Microsoft по предложению FROM это сформулировано так: «Хотя размещение таких предикатов не имеет разницы для внутренних соединений, они могут привести к другому результату при использовании внешних соединений».
Для LEFT JOIN она меняет результат. Задача: показать всех сотрудников и их заказы дороже 4 500 ₽.
Фильтр в WHERE:
SELECT e.name, o.amount
FROM employees AS e
LEFT JOIN orders AS o ON o.employee_id = e.id
WHERE o.amount > 4500
ORDER BY e.id, o.id;
name | amount
------+--------
Анна | 5000
Анна | 7000
Остальные сотрудники исчезли. WHERE срабатывает после соединения (порядок выполнения частей запроса разобран в статье про SELECT), а у тех, кому пары не нашлось, amount равен NULL, и сравнение NULL > 4500 не проходит. Левое соединение по факту стало внутренним.
Решение: перенести условие в ON.
SELECT e.name, o.amount
FROM employees AS e
LEFT JOIN orders AS o ON o.employee_id = e.id AND o.amount > 4500
ORDER BY e.id, o.id;
name | amount
-------+--------
Анна | 5000
Анна | 7000
Борис | NULL
Вера | NULL
Глеб | NULL
Правило простое. Условие про то, какие строки правой таблицы подцеплять, пишите в ON. Условие про то, какие строки оставить в итоге, пишите в WHERE.
Частые ошибки и что с ними делать
- Неоднозначный столбец.
SELECT name FROM employees JOIN departments ON …падает:nameесть в обеих таблицах. MySQL пишетERROR 1052 (23000): Column 'name' in field list is ambiguous, PostgreSQL пишетcolumn reference "name" is ambiguous. Решение: указать таблицу или псевдоним,e.name. - Задвоенные суммы и счётчики. После соединения с таблицей «многие» строки размножаются. Запрос «сколько сотрудников с заказами в отделе» через
COUNT(e.id)покажет у «Продаж» 2, хотя с заказами там одна Анна. ПомогаетCOUNT(DISTINCT e.id)или агрегирование заказов в подзапросе до соединения. Варианты подсчёта подробно разобраны в статье о функции COUNT. COUNT(*)после LEFT JOIN. Для отдела без сотрудниковCOUNT(*)вернёт 1: строка-то в результате есть, просто заполнена NULL.COUNT(e.id)вернёт честный 0.- NULL в ключах не соединяются. Две строки с NULL в столбце соединения не считаются парой. В справке Microsoft: «Если в столбцах таблиц, которые объединяются, есть значения NULL, то эти значения не совпадают друг с другом» (статья «Соединения» для SQL Server).
- Забытое ON. В PostgreSQL
FROM employees JOIN departmentsбез условия даёт синтаксическую ошибку. MySQL такой запрос выполнит и вернёт все комбинации: в его справке сказано, чтоJOIN,CROSS JOINиINNER JOIN«are syntactic equivalents». На двух таблицах по миллиону строк это триллион строк результата.
JOIN или UNION: в чём разница
Оба называют «объединением таблиц», но делают разное:
| JOIN | UNION | |
|---|---|---|
| Как складывает данные | по горизонтали: к строке добавляются столбцы другой таблицы | по вертикали: строки второго запроса идут под строками первого |
| Что нужно | условие связи (ключ) | одинаковое число и совместимые типы столбцов |
| Пример | сотрудник + название его отдела | клиенты из двух филиалов одним списком |
Если нужно приписать к записи сведения из другой таблицы, это JOIN. Если два одинаковых по структуре списка нужно поставить друг под другом, это UNION. Для подсчётов по группам внутри соединённых данных пригодятся оконные функции, а для меток по условию оператор CASE.
Где освоить SQL системно
На соединениях проверяют и стажёров, и аналитиков с опытом: без них не собрать отчёт из нормализованной базы. Чтобы пройти путь от SELECT до оптимизации запросов с практикой и проверкой заданий, посмотрите курсы по SQL в нашем каталоге. Ниже пять программ с ценами и сроками, сравнить их можно на странице подборки.
| Курс | Школа | Стоимость со скидкой | В рассрочку | Длительность | Обзор курса от Checkroi |
|---|---|---|---|---|---|
| Нейросети для рабочих задач Перейти на сайт курса | 31 290 ₽ | - | 1 месяц | Обзор курса | |
| SQL для анализа данных Перейти на сайт курса | 52 353 ₽ | 6203 ₽/мес. | 2 месяца | Обзор курса | |
| SQL-разработчик: тариф PRO Перейти на сайт курса | 89 900 ₽ | 7491 ₽/мес. | 2 месяца | Обзор курса | |
| SQL-разработчик: тариф Базовый Перейти на сайт курса | 79 900 ₽ | 3329 ₽/мес. | 3 месяца | Обзор курса | |
| SQL с нуля | 134 640 ₽ | 5500 ₽/мес. | 12 месяцев | Обзор курса |
Больше программ — в полном каталоге курсов по SQL
Если хотите работать с базами данных профессионально, загляните в план «Как стать SQL-разработчиком»: там расписано, что учить по месяцам и с чем идти на первое собеседование.




