Типы 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. От этого зависит, какой тип нужен и нужен ли агрегат.

Читайте также

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.