Типы JOIN в SQL: шпаргалка с примерами
Зачем аналитику глубокое понимание JOIN
50% багов в аналитических отчётах — из-за неправильного JOIN. Дублирование строк, потеря данных, неправильные агрегаты — почти всегда сводится к выбору типа или ключа.
На собесе типы JOIN — обязательный блок. Если путаетесь в LEFT vs RIGHT или не понимаете, как работает FULL OUTER — интервьюер это заметит через 30 секунд.
Основные типы
| JOIN | Что возвращает |
|---|---|
| INNER | Только совпавшие строки из обеих таблиц |
| LEFT (OUTER) | Все из левой + совпавшие из правой |
| RIGHT (OUTER) | Все из правой + совпавшие из левой |
| FULL (OUTER) | Все из обеих + NULL где не совпало |
| CROSS | Декартово произведение |
| SELF | JOIN таблицы с самой собой |
INNER JOIN
SELECT u.name, o.amount
FROM users u
INNER JOIN orders o ON o.user_id = u.user_id;- Возвращает только пользователей, у которых есть заказы.
- Заказов без привязки к users (если user_id NULL) тоже не будет.
- Если у пользователя 3 заказа — 3 строки.
Главная ловушка: INNER JOIN может уменьшить количество строк левой таблицы, если в правой нет совпадений.
LEFT JOIN
SELECT u.name, o.amount
FROM users u
LEFT JOIN orders o ON o.user_id = u.user_id;- Возвращает всех пользователей.
- У тех, у кого нет заказов,
o.amount = NULL.
Главная ловушка: если поставить WHERE o.status = 'paid' — LEFT JOIN превращается в INNER. Потому что NULL = 'paid' даёт UNKNOWN и строка отфильтровывается.
Правильно: условие на правую таблицу в ON:
SELECT u.name, o.amount
FROM users u
LEFT JOIN orders o ON o.user_id = u.user_id AND o.status = 'paid';
-- Все пользователи, но amount только для paid-заказовRIGHT JOIN
Зеркальная версия LEFT JOIN. На практике почти не используется — проще поменять таблицы местами:
-- Эти два запроса эквивалентны
SELECT * FROM a LEFT JOIN b ON a.id = b.id;
SELECT * FROM b RIGHT JOIN a ON a.id = b.id;Совет: всегда пишите LEFT. Это читабельнее, и код консистентнее.
Тренироваться на таких вопросах можно в Telegram-боте Карьерник — там 1500+ задач с реальных собесов с разборами.
FULL OUTER JOIN
SELECT COALESCE(u.user_id, o.user_id) AS user_id, u.name, o.amount
FROM users u
FULL OUTER JOIN orders o ON o.user_id = u.user_id;Возвращает:
- Всех пользователей (даже без заказов) — с
o.amount = NULL. - Все заказы (даже orphan-записи без связки) — с
u.name = NULL.
Частый случай — сверка данных между системами: где расхождения.
CROSS JOIN
Декартово произведение — каждая строка левой с каждой строкой правой:
SELECT * FROM categories CROSS JOIN products;
-- Если 10 категорий и 1000 товаров — 10 000 строкПрактическое применение: генерация сетки (все категории × все дни месяца).
Ловушка: случайный CROSS JOIN когда забыли условие:
-- ❌ Декартово произведение, а не JOIN
SELECT * FROM users u, orders o WHERE u.name = 'Иван';SELF JOIN
JOIN таблицы с самой собой — для иерархий и сравнения соседних строк:
-- Найти менеджеров и их подчинённых
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON m.id = e.manager_id;Или сравнение заказов подряд:
SELECT o1.user_id, o1.amount AS curr, o2.amount AS prev
FROM orders o1
LEFT JOIN orders o2
ON o2.user_id = o1.user_id
AND o2.created_at = o1.created_at - INTERVAL '1 day';На практике SELF JOIN часто заменяется оконной функцией LAG.
ANTI JOIN и SEMI JOIN
Не отдельные операторы, а паттерны.
ANTI JOIN — найти «кто НЕ матчится»
-- Пользователи без заказов
SELECT u.*
FROM users u
LEFT JOIN orders o ON o.user_id = u.user_id
WHERE o.user_id IS NULL;
-- Или через NOT EXISTS
SELECT u.* FROM users u
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.user_id);SEMI JOIN — «кто матчится хотя бы раз»
-- Пользователи с хотя бы одним заказом (без дубликатов строк)
SELECT u.*
FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.user_id);USING vs ON
-- Эквивалентные записи, если столбец называется одинаково
SELECT * FROM users JOIN orders ON users.user_id = orders.user_id;
SELECT * FROM users JOIN orders USING (user_id);USING короче и в SELECT столбец user_id будет один, а не два. На собесе можно упомянуть как знак владения синтаксисом.
NATURAL JOIN — почему не использовать
SELECT * FROM users NATURAL JOIN orders;
-- Автоматически матчит по всем одноимённым столбцамВыглядит удобно, но если в таблицах есть «случайные» совпадающие столбцы (created_at, is_active) — NATURAL JOIN их тоже возьмёт, и запрос начнёт возвращать чушь. Никогда не используйте в продакшне.
К слову, набить руку на таких кейсах удобно через тренажёр в Telegram — разбирайте по 10 вопросов в день, через 2 недели тема становится рефлексом.
Частые ловушки
1. Дублирование строк при JOIN
-- 100 пользователей + 500 заказов = ?
SELECT u.user_id, SUM(o.amount)
FROM users u
LEFT JOIN orders o USING (user_id)
GROUP BY u.user_id;Если у одного пользователя 5 заказов — запрос вернёт 5 строк до GROUP BY. Результат после SUM всё равно правильный — но если бы не было GROUP BY, данные были бы раздуты.
2. JOIN на двух отношениях n:m
-- users (1) ↔ orders (n) ↔ order_items (m)
SELECT u.name, SUM(oi.price)
FROM users u
JOIN orders o USING (user_id)
JOIN order_items oi USING (order_id);Одна строка users → много orders → много order_items. Если сгруппировать по user_id — SUM будет правильный. Но COUNT(DISTINCT o.order_id) — нет, если не быть аккуратным.
Решение: агрегат сначала до JOIN:
WITH order_totals AS (
SELECT order_id, SUM(price) AS total FROM order_items GROUP BY order_id
)
SELECT u.name, SUM(t.total)
FROM users u
JOIN orders o USING (user_id)
JOIN order_totals t USING (order_id)
GROUP BY u.name;3. LEFT JOIN превращается в INNER
См. выше — если условие на правую таблицу попало в WHERE, а не в ON.
10 задач на JOIN
1. Все пользователи и их суммарные заказы (включая 0)
SELECT u.name, COALESCE(SUM(o.amount), 0) AS total
FROM users u
LEFT JOIN orders o USING (user_id)
GROUP BY u.name;2. Заказы с именем пользователя
SELECT o.order_id, u.name, o.amount
FROM orders o
JOIN users u USING (user_id);3. Пользователи без заказов
SELECT u.*
FROM users u
LEFT JOIN orders o USING (user_id)
WHERE o.user_id IS NULL;4. Товары, которые никто не купил
SELECT p.*
FROM products p
LEFT JOIN order_items oi USING (product_id)
WHERE oi.product_id IS NULL;5. Пары заказов одного пользователя
SELECT o1.order_id, o2.order_id
FROM orders o1
JOIN orders o2 ON o1.user_id = o2.user_id AND o1.order_id < o2.order_id;6. Иерархия сотрудников
SELECT e.name, m.name AS manager
FROM employees e
LEFT JOIN employees m ON m.id = e.manager_id;7. Разница между текущим и предыдущим заказом
SELECT o1.user_id, o1.amount - o2.amount AS diff
FROM orders o1
JOIN orders o2
ON o2.user_id = o1.user_id
AND o2.created_at = o1.created_at - INTERVAL '1 day';8. Полная матрица категорий × дней месяца
SELECT c.category, d.day
FROM categories c
CROSS JOIN generate_series('2026-04-01'::DATE, '2026-04-30'::DATE, '1 day'::INTERVAL) d(day);9. Сверка двух источников
SELECT COALESCE(a.id, b.id) AS id,
a.value AS source_a, b.value AS source_b
FROM source_a a
FULL OUTER JOIN source_b b ON b.id = a.id
WHERE a.value IS DISTINCT FROM b.value;10. Пользователи с заказами, но без оплат
SELECT DISTINCT u.user_id
FROM users u
JOIN orders o USING (user_id)
LEFT JOIN payments p USING (order_id)
WHERE p.payment_id IS NULL;Как тренироваться
JOIN — тема, где теория учится за час, а навык «не налажать» — месяцами. Тренируйтесь на задачах, где есть 3-4 таблицы и разные связи.
Тренажёр Карьерник содержит блок на JOIN: дублирование строк, ANTI JOIN, SELF JOIN, FULL OUTER для сверок. Каждая задача с разбором типичных ошибок.
Совет: на собесе, прежде чем писать JOIN, спросите «а какая кардинальность у этих таблиц?» 1:1, 1:n, m:n. От этого зависит, какой тип нужен и нужен ли агрегат.
Читайте также
- JOIN SQL: шпаргалка
- JOIN SQL: гайд
- LEFT JOIN vs INNER JOIN
- LEFT JOIN vs RIGHT JOIN
- CROSS JOIN и SELF JOIN
FAQ
Чем LEFT JOIN отличается от LEFT OUTER JOIN?
Ничем. OUTER — ключевое слово, которое можно опустить. LEFT JOIN = LEFT OUTER JOIN. Аналогично для RIGHT и FULL.
Можно ли несколько условий в ON?
Да: ON a.x = b.x AND a.y = b.y. Или ON a.x = b.x AND b.status = 'active'. Второе особенно важно для LEFT JOIN — см. секцию про ловушки.
Что быстрее — JOIN или IN?
В современных СУБД оптимизатор обычно превращает их в одинаковые планы. JOIN чаще быстрее для больших таблиц (hash join), IN — для маленьких списков. Но на собесе лучше говорить «зависит от размера таблиц и наличия индекса», чем одним правилом.
FULL OUTER JOIN — когда реально нужен?
Редко. Основные случаи: сверка двух источников данных, объединение полной временной оси с наличными записями, сравнение версий таблиц. В продуктовой аналитике обычно LEFT или INNER.