SQL JOIN за 10 минут: наглядный гид с реальными примерами
Объясняем INNER, LEFT, RIGHT и FULL JOIN на живых таблицах — без воды и абстрактных схем, только запросы и результаты.
Содержание
SQL JOIN за 10 минут: наглядный гид с реальными примерами
Большинство проблем с JOIN возникают не потому, что концепция сложная. Она действительно простая. Проблемы возникают потому, что её объясняют через абстрактные диаграммы Венна, которые красиво выглядят на слайдах, но не помогают понять, что происходит с реальными строками в реальных таблицах.
В этом материале мы разберём все четыре основных типа JOIN — INNER, LEFT, RIGHT и FULL — на одном наборе данных. Вы увидите конкретные запросы и конкретные результаты. К концу статьи у вас будет рабочая модель в голове, а не просто запомненные термины.
Данные, с которыми мы будем работать
Возьмём классический, но реалистичный сценарий: интернет-магазин. У нас есть две таблицы — customers (покупатели) и orders (заказы).
Таблица customers:
| customer_id | name | city |
|---|---|---|
| 1 | Анна Смирнова | Москва |
| 2 | Иван Петров | Санкт-Петербург |
| 3 | Мария Козлова | Казань |
| 4 | Дмитрий Орлов | Новосибирск |
Таблица orders:
| order_id | customer_id | amount | product |
|---|---|---|---|
| 101 | 1 | 4500.00 | Ноутбук |
| 102 | 2 | 1200.00 | Мышь |
| 103 | 1 | 890.00 | Клавиатура |
| 104 | 5 | 3200.00 | Монитор |
Обратите внимание на намеренно созданные «несоответствия»: у Марии Козловой и Дмитрия Орлова нет заказов. Заказ с customer_id = 5 ссылается на несуществующего покупателя. Именно эти случаи и делают выбор типа JOIN принципиальным.
Создадим таблицы и заполним их:
CREATE TABLE customers (
customer_id INT PRIMARY KEY,
name VARCHAR(100),
city VARCHAR(100)
);
CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_id INT,
amount DECIMAL(10, 2),
product VARCHAR(100)
);
INSERT INTO customers VALUES
(1, 'Анна Смирнова', 'Москва'),
(2, 'Иван Петров', 'Санкт-Петербург'),
(3, 'Мария Козлова', 'Казань'),
(4, 'Дмитрий Орлов', 'Новосибирск');
INSERT INTO orders VALUES
(101, 1, 4500.00, 'Ноутбук'),
(102, 2, 1200.00, 'Мышь'),
(103, 1, 890.00, 'Клавиатура'),
(104, 5, 3200.00, 'Монитор');
INNER JOIN: только совпадения
INNER JOIN — самый распространённый тип. Он возвращает строки, для которых нашлось совпадение в обеих таблицах по условию соединения.
SELECT
c.customer_id,
c.name,
o.order_id,
o.product,
o.amount
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id;
Результат:
| customer_id | name | order_id | product | amount |
|---|---|---|---|---|
| 1 | Анна Смирнова | 101 | Ноутбук | 4500.00 |
| 1 | Анна Смирнова | 103 | Клавиатура | 890.00 |
| 2 | Иван Петров | 102 | Мышь | 1200.00 |
Что произошло: Мария Козлова и Дмитрий Орлов выпали — у них нет заказов. Заказ 104 тоже выпал — у него нет соответствующего покупателя. В результат попали только те строки, где customer_id нашёлся в обеих таблицах одновременно.
Когда использовать: когда вас интересуют только «полные» данные. Например, отчёт о продажах, где нужны имена покупателей и суммы их заказов — без пустых строк.
Типичная ошибка: использовать INNER JOIN там, где нужен LEFT JOIN, и потом удивляться, почему часть данных «пропала». Если вы делаете аналитику и хотите видеть всех покупателей, включая тех, кто ничего не купил, — INNER JOIN вас подведёт.
LEFT JOIN: все из левой таблицы
LEFT JOIN (или LEFT OUTER JOIN — это синонимы) возвращает все строки из левой таблицы и совпадающие строки из правой. Если совпадения нет — правая часть заполняется NULL.
SELECT
c.customer_id,
c.name,
o.order_id,
o.product,
o.amount
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id;
Результат:
| customer_id | name | order_id | product | amount |
|---|---|---|---|---|
| 1 | Анна Смирнова | 101 | Ноутбук | 4500.00 |
| 1 | Анна Смирнова | 103 | Клавиатура | 890.00 |
| 2 | Иван Петров | 102 | Мышь | 1200.00 |
| 3 | Мария Козлова | NULL | NULL | NULL |
| 4 | Дмитрий Орлов | NULL | NULL | NULL |
Теперь все покупатели в результате. Мария и Дмитрий есть, но их столбцы из таблицы orders — пустые. Заказ 104 по-прежнему отсутствует, потому что его customer_id = 5 не существует в левой таблице.
Практический сценарий — найти покупателей без заказов:
SELECT
c.customer_id,
c.name
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;
Результат:
| customer_id | name |
|---|---|
| 3 | Мария Козлова |
| 4 | Дмитрий Орлов |
Это классический паттерн: LEFT JOIN + фильтр по NULL в правой таблице. Он работает быстрее и читается чище, чем вложенный NOT IN или NOT EXISTS в большинстве случаев.
Когда использовать: практически всегда, когда вы хотите сохранить все записи из основной таблицы. Аналитика активности пользователей, отчёты с нулевыми значениями, поиск «сирот» в данных — всё это LEFT JOIN.
RIGHT JOIN: все из правой таблицы
RIGHT JOIN — зеркальное отражение LEFT JOIN. Возвращает все строки из правой таблицы и совпадающие из левой.
SELECT
c.customer_id,
c.name,
o.order_id,
o.product,
o.amount
FROM customers c
RIGHT JOIN orders o ON c.customer_id = o.customer_id;
Результат:
| customer_id | name | order_id | product | amount |
|---|---|---|---|---|
| 1 | Анна Смирнова | 101 | Ноутбук | 4500.00 |
| 2 | Иван Петров | 102 | Мышь | 1200.00 |
| 1 | Анна Смирнова | 103 | Клавиатура | 890.00 |
| NULL | NULL | 104 | Монитор | 3200.00 |
Теперь все заказы в результате, включая заказ 104 с несуществующим покупателем. Мария и Дмитрий снова выпали — у них нет заказов в правой таблице.
Честный комментарий: RIGHT JOIN используется редко. Большинство разработчиков просто меняют порядок таблиц и пишут LEFT JOIN — это читается естественнее. Следующие два запроса дают идентичный результат:
-- Вариант с RIGHT JOIN
SELECT * FROM customers c
RIGHT JOIN orders o ON c.customer_id = o.customer_id;
-- Эквивалентный вариант с LEFT JOIN (более распространённый стиль)
SELECT * FROM orders o
LEFT JOIN customers c ON c.customer_id = o.customer_id;
Если в вашей команде нет жёсткого соглашения по стилю — придерживайтесь LEFT JOIN. Код будет однороднее и понятнее при чтении.
FULL JOIN: все строки из обеих таблиц
FULL JOIN (или FULL OUTER JOIN) возвращает все строки из обеих таблиц. Где совпадений нет — ставит NULL с соответствующей стороны.
SELECT
c.customer_id AS c_id,
c.name,
o.order_id,
o.customer_id AS o_cid,
o.product,
o.amount
FROM customers c
FULL JOIN orders o ON c.customer_id = o.customer_id;
Результат:
| c_id | name | order_id | o_cid | product | amount |
|---|---|---|---|---|---|
| 1 | Анна Смирнова | 101 | 1 | Ноутбук | 4500.00 |
| 1 | Анна Смирнова | 103 | 1 | Клавиатура | 890.00 |
| 2 | Иван Петров | 102 | 2 | Мышь | 1200.00 |
| 3 | Мария Козлова | NULL | NULL | NULL | NULL |
| 4 | Дмитрий Орлов | NULL | NULL | NULL | NULL |
| NULL | NULL | 104 | 5 | Монитор | 3200.00 |
Теперь видно всё: и покупатели без заказов, и заказ без покупателя. Это максимально полная картина состояния данных.
Важно: FULL JOIN поддерживается не во всех СУБД. MySQL, например, его не поддерживает напрямую. Там используют эмуляцию через UNION:
-- Эмуляция FULL JOIN в MySQL
SELECT c.customer_id, c.name, o.order_id, o.product, o.amount
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
UNION
SELECT c.customer_id, c.name, o.order_id, o.product, o.amount
FROM customers c
RIGHT JOIN orders o ON c.customer_id = o.customer_id
WHERE c.customer_id IS NULL;
PostgreSQL, SQL Server и Oracle поддерживают FULL JOIN нативно.
Когда использовать: при аудите данных, поиске несоответствий между таблицами, синхронизации данных из разных источников. В повседневной аналитике FULL JOIN встречается реже, чем INNER и LEFT, но когда он нужен — заменить его нечем.
Агрегация поверх JOIN: реальный рабочий запрос
Понимание JOIN по-настоящему проверяется, когда вы начинаете комбинировать их с агрегатными функциями. Вот типичная задача: получить список всех покупателей с суммой их заказов, включая тех, кто ещё ничего не купил.
SELECT
c.customer_id,
c.name,
c.city,
COUNT(o.order_id) AS order_count,
COALESCE(SUM(o.amount), 0) AS total_spent
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.name, c.city
ORDER BY total_spent DESC;
Результат:
| customer_id | name | city | order_count | total_spent |
|---|---|---|---|---|
| 1 | Анна Смирнова | Москва | 2 | 5390.00 |
| 2 | Иван Петров | Санкт-Петербург | 1 | 1200.00 |
| 3 | Мария Козлова | Казань | 0 | 0.00 |
| 4 | Дмитрий Орлов | Новосибирск | 0 | 0.00 |
Два момента, на которые стоит обратить внимание:
COUNT(o.order_id)считает только не-NULL значения, поэтому для покупателей без заказов возвращает 0, а не 1.COALESCE(SUM(o.amount), 0)заменяет NULL на 0. Без него покупатели без заказов показали бы NULL в колонкеtotal_spent, что некорректно для финансового отчёта.
Если вы только начинаете систематически разбираться с SQL — от базового синтаксиса до таких запросов — стоит посмотреть на структурированный курс. Например, «SQL – для начинающих!» от sudo teach IT строится именно на практике с реальными данными, а не на теоретических определениях.
Частые ошибки при работе с JOIN
Декартово произведение вместо JOIN
Если забыть условие ON или написать его неверно, получите декартово произведение — каждая строка левой таблицы соединится с каждой строкой правой. При 1000 строк в каждой таблице результат будет содержать миллион строк.
-- Опасный запрос — отсутствует условие соединения
SELECT * FROM customers, orders;
-- Вернёт 4 × 4 = 16 строк вместо ожидаемых
-- Правильно
SELECT * FROM customers c
JOIN orders o ON c.customer_id = o.customer_id;
Неоднозначные имена столбцов
Когда обе таблицы содержат столбец с одинаковым именем, без псевдонима запрос либо упадёт с ошибкой, либо вернёт неожиданный результат.
-- Плохо: неясно, из какой таблицы customer_id
SELECT customer_id, name, amount
FROM customers
JOIN orders ON customers.customer_id = orders.customer_id;
-- Хорошо: явные псевдонимы таблиц
SELECT c.customer_id, c.name, o.amount
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id;
JOIN по неиндексированным столбцам
Технически запрос работает, но на больших таблицах производительность падает катастрофически. Столбцы, по которым вы делаете JOIN, должны быть проиндексированы — как правило, это первичные и внешние ключи.
-- Убедитесь, что индекс на внешнем ключе существует
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
Быстрая шпаргалка
| Тип JOIN | Что возвращает |
|---|---|
| INNER JOIN | Только строки с совпадениями в обеих таблицах |
| LEFT JOIN | Все строки из левой + совпадения из правой (NULL если нет) |
| RIGHT JOIN | Все строки из правой + совпадения из левой (NULL если нет) |
| FULL JOIN | Все строки из обеих таблиц (NULL там, где нет совпадения) |
Итог
JOIN — это не синтаксис, который нужно зазубрить. Это инструмент, который нужно понять через данные. Задайте себе один вопрос перед написанием запроса: «Что должно произойти со строками, у которых нет пары?» Если они должны исчезнуть — INNER JOIN. Если остаться — LEFT или FULL JOIN в зависимости от того, строки из какой таблицы важнее.
Практика на реальных данных быстро закрепляет эту логику. Создайте свои тестовые таблицы, намеренно добавьте несоответствия — как мы сделали с заказом 104 — и посмотрите, как ведут себя разные типы JOIN. Это работает лучше любых диаграмм.
Если хотите пройти путь от основ до уверенной работы с запросами системно, курс «SQL – для начинающих!» — один из разумных способов структурировать это обучение с практическими заданиями на каждом шаге.
Комментарии
Пока нет комментариев