JOIN в SQL: шпаргалка для собеседования

Зачем аналитику разбираться в JOIN

JOIN — фундамент SQL. Без него вы не напишете ни один рабочий запрос на собеседовании.

Реальные данные всегда лежат в нескольких таблицах. Заказы отдельно, клиенты отдельно, продукты отдельно. JOIN — механизм, который позволяет соединять таблицы между собой по общему ключу. На техническом собеседовании аналитика JOIN встречается в каждой второй задаче, и недостаточно просто знать слово LEFT JOIN — нужно понимать, что произойдёт с данными в каждом конкретном случае.

Если вы готовитесь к собеседованию по SQL, эта шпаргалка — то, что стоит прочитать первым.

Для всех примеров используем две таблицы:

employees — сотрудники:

id name department_id
1 Анна 10
2 Борис 20
3 Вера 30
4 Глеб NULL

departments — отделы:

id dept_name
10 Аналитика
20 Маркетинг
40 Финансы

Обратите внимание: Вера ссылается на отдел 30, которого нет в departments. Глеб вообще без отдела. Отдел «Финансы» (40) не имеет сотрудников. Эти несовпадения — ключ к пониманию разницы между типами JOIN.

INNER JOIN

Возвращает только те строки, для которых есть совпадение в обеих таблицах.

SELECT
    e.name,
    d.dept_name
FROM employees e
INNER JOIN departments d ON e.department_id = d.id

Результат:

name dept_name
Анна Аналитика
Борис Маркетинг

Вера (отдел 30 не найден), Глеб (department_id = NULL) и Финансы (нет сотрудников) — все выпали.

Когда использовать: когда нужны только строки с полным совпадением. Например, «покажите все заказы с названиями товаров» — если товар удалён, заказ можно опустить.

Подводный камень: INNER JOIN молча убирает строки без совпадений. Если в данных есть NULL или неконсистентные ключи, вы потеряете записи и даже не заметите. Всегда проверяйте, сколько строк было до и после JOIN.

LEFT JOIN

Возвращает все строки из левой таблицы. Если совпадения в правой нет — подставляет NULL.

SELECT
    e.name,
    d.dept_name
FROM employees e
LEFT JOIN departments d ON e.department_id = d.id

Результат:

name dept_name
Анна Аналитика
Борис Маркетинг
Вера NULL
Глеб NULL

Все сотрудники на месте. Вера и Глеб получили NULL в dept_name, потому что совпадения не нашлось.

Когда использовать: самый частый тип JOIN в аналитике. «Покажите всех пользователей и их заказы, включая тех, кто ничего не покупал» — это LEFT JOIN.

Подводный камень: если в правой таблице несколько строк с одним ключом, LEFT JOIN создаст дубликаты. Один сотрудник может превратиться в три строки, если у отдела три записи с одним id. Проверяйте уникальность ключа.

RIGHT JOIN

Зеркало LEFT JOIN — все строки из правой таблицы, совпадения из левой.

SELECT
    e.name,
    d.dept_name
FROM employees e
RIGHT JOIN departments d ON e.department_id = d.id

Результат:

name dept_name
Анна Аналитика
Борис Маркетинг
NULL Финансы

Финансы остались, хотя сотрудников нет. Вера и Глеб выпали — их отделов нет в departments.

Когда использовать: на практике почти никогда. Любой RIGHT JOIN можно переписать как LEFT JOIN, поменяв таблицы местами. Так читабельнее.

Подводный камень: на собеседовании RIGHT JOIN спрашивают, чтобы проверить понимание. Знайте, что он существует, но в рабочем коде используйте LEFT JOIN.

FULL OUTER JOIN

Возвращает все строки из обеих таблиц. Где совпадений нет — NULL.

SELECT
    e.name,
    d.dept_name
FROM employees e
FULL OUTER JOIN departments d ON e.department_id = d.id

Результат:

name dept_name
Анна Аналитика
Борис Маркетинг
Вера NULL
Глеб NULL
NULL Финансы

Ни одна строка не потеряна — ни из employees, ни из departments.

Когда использовать: для аудита данных и поиска несовпадений. «Какие сотрудники без отдела и какие отделы без сотрудников?» — классическая задача на FULL JOIN.

Подводный камень: не все СУБД поддерживают FULL OUTER JOIN. MySQL, например, не поддерживает — придётся эмулировать через UNION двух LEFT JOIN. PostgreSQL, SQL Server и BigQuery поддерживают.

CROSS JOIN

Декартово произведение — каждая строка левой таблицы соединяется с каждой строкой правой. Условие ON не указывается.

SELECT
    e.name,
    d.dept_name
FROM employees e
CROSS JOIN departments d

Результат: 4 сотрудника x 3 отдела = 12 строк. Каждый сотрудник соединён с каждым отделом.

Когда использовать: для генерации всех комбинаций. Пример — построить календарную сетку: все даты x все пользователи, чтобы потом через LEFT JOIN найти дни без активности.

SELECT
    dates.dt,
    users.user_id,
    a.event_count
FROM dates
CROSS JOIN users
LEFT JOIN activity a
    ON a.user_id = users.user_id AND a.event_date = dates.dt

Подводный камень: CROSS JOIN плодит строки. 10 000 пользователей x 365 дней = 3 650 000 строк. Если не ожидали такого объёма — запрос повесит базу. Всегда считайте мощность результата заранее.

SELF JOIN

Таблица соединяется сама с собой. Это не отдельный тип синтаксиса — используется любой JOIN, просто обе стороны ссылаются на одну таблицу с разными алиасами.

SELECT
    e.name AS employee,
    m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id

Когда использовать: иерархии (сотрудник — руководитель), цепочки событий (предыдущий заказ — текущий заказ), поиск дубликатов.

Подводный камень: без алиасов запрос не скомпилируется — SQL не поймёт, какой экземпляр таблицы вы имеете в виду. Всегда задавайте алиасы.

WHERE vs ON: куда писать условие

Это один из самых коварных вопросов на собеседовании. Разница проявляется только при LEFT/RIGHT/FULL JOIN.

Условие в ON — фильтрует до соединения:

SELECT e.name, d.dept_name
FROM employees e
LEFT JOIN departments d
    ON e.department_id = d.id
    AND d.dept_name = 'Аналитика'

Результат: все 4 сотрудника. Но dept_name заполнен только у Анны — остальные получили NULL, потому что условие не прошло при соединении.

Условие в WHERE — фильтрует после соединения:

SELECT e.name, d.dept_name
FROM employees e
LEFT JOIN departments d ON e.department_id = d.id
WHERE d.dept_name = 'Аналитика'

Результат: только Анна. WHERE отрезал все строки, где dept_name не равен «Аналитика» — включая NULL. LEFT JOIN фактически превратился в INNER JOIN.

Правило: если фильтр относится к правой таблице LEFT JOIN и вы хотите сохранить все строки левой — ставьте условие в ON. Если хотите жёсткую фильтрацию — в WHERE.

Множественные JOIN

В реальных запросах часто соединяют 3–5 таблиц. Порядок чтения — сверху вниз, каждый следующий JOIN добавляет данные к результату предыдущего.

SELECT
    o.order_id,
    c.customer_name,
    p.product_name,
    o.quantity
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN products p ON o.product_id = p.id
JOIN warehouses w ON p.warehouse_id = w.id
WHERE w.city = 'Москва'

Совет: начинайте с основной таблицы (факт-таблицы), затем подтягивайте справочники. Это удобнее для чтения и отладки.

Вопросы с собеседований

1. Чем INNER JOIN отличается от LEFT JOIN?

INNER JOIN возвращает только строки с совпадениями в обеих таблицах. LEFT JOIN возвращает все строки из левой таблицы — если совпадения в правой нет, подставляется NULL. На практике это значит, что LEFT JOIN не теряет данные из основной таблицы, а INNER JOIN может молча отбросить строки.

2. Как найти строки, которые есть в одной таблице, но отсутствуют в другой?

LEFT JOIN + WHERE ... IS NULL:

SELECT e.name
FROM employees e
LEFT JOIN departments d ON e.department_id = d.id
WHERE d.id IS NULL

Альтернатива — NOT EXISTS или NOT IN, но LEFT JOIN + IS NULL читается проще и на больших таблицах обычно работает не хуже.

3. Что произойдёт, если в JOIN-условии есть дубликаты ключей?

Если в одной из таблиц ключ не уникален, JOIN создаст декартово произведение по этому ключу. Одна строка из левой таблицы соединится с каждой подходящей строкой правой. Если в orders 1 заказ, а в order_items 3 позиции — получите 3 строки. Если дубликаты в обеих таблицах — строк станет ещё больше. Это частая причина «раздутых» результатов.

4. Можно ли соединять таблицы по нескольким столбцам?

Да. Условие ON может содержать несколько столбцов через AND:

SELECT *
FROM sales s
JOIN targets t
    ON s.region = t.region
    AND s.month = t.month

Это составной ключ — соединение происходит только при совпадении обоих столбцов.

5. В чём разница между WHERE и ON при LEFT JOIN?

Условие в ON фильтрует правую таблицу до соединения — строки левой таблицы без совпадений сохраняются с NULL. Условие в WHERE фильтрует после соединения — строки с NULL отбрасываются, и LEFT JOIN фактически превращается в INNER JOIN. На собеседовании это спрашивают, чтобы проверить, понимаете ли вы порядок выполнения SQL-запроса.

6. Напишите запрос: для каждого пользователя покажите количество заказов, включая пользователей с нулём заказов.

SELECT
    u.user_id,
    u.name,
    COUNT(o.order_id) AS order_count
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_id, u.name
ORDER BY order_count DESC

Ключевой момент: COUNT(o.order_id), а не COUNT(). COUNT() посчитает строки с NULL как 1, и пользователи без заказов получат order_count = 1 вместо 0. COUNT по столбцу правой таблицы игнорирует NULL — это правильное поведение.

10 задач на JOIN с собеседований

Задача 1. Пользователи без заказов (anti-join)

SELECT u.user_id, u.name
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE o.order_id IS NULL;

LEFT JOIN + WHERE IS NULL — стандартный паттерн anti-join. Альтернатива: NOT EXISTS.

Задача 2. Пользователи, которые покупали и в категории A, и в категории B

SELECT DISTINCT o1.user_id
FROM orders o1
JOIN order_items oi1 ON o1.order_id = oi1.order_id
JOIN products p1 ON oi1.product_id = p1.product_id AND p1.category = 'A'
JOIN orders o2 ON o1.user_id = o2.user_id
JOIN order_items oi2 ON o2.order_id = oi2.order_id
JOIN products p2 ON oi2.product_id = p2.product_id AND p2.category = 'B';

Множественные JOIN + self-join по user_id. Альтернатива: два подзапроса с INTERSECT.

Задача 3. Последний заказ каждого пользователя

SELECT o.*
FROM orders o
JOIN (
  SELECT user_id, MAX(created_at) AS max_date
  FROM orders GROUP BY user_id
) latest ON o.user_id = latest.user_id AND o.created_at = latest.max_date;

JOIN с подзапросом — частый паттерн. Альтернатива: ROW_NUMBER с PARTITION BY.

Задача 4. Товары с ценой выше средней в своей категории

SELECT p.product_id, p.name, p.price, avg_prices.avg_price
FROM products p
JOIN (
  SELECT category, AVG(price) AS avg_price
  FROM products GROUP BY category
) avg_prices ON p.category = avg_prices.category
WHERE p.price > avg_prices.avg_price;

Задача 5. Self-join: найти пары пользователей из одного города

SELECT u1.name AS user1, u2.name AS user2, u1.city
FROM users u1
JOIN users u2 ON u1.city = u2.city AND u1.user_id < u2.user_id;

Условие u1.user_id < u2.user_id исключает дубли и пары с самим собой.

Задача 6. FULL JOIN: все продукты и все заказы

SELECT
  COALESCE(p.product_id, oi.product_id) AS product_id,
  p.name,
  COUNT(oi.item_id) AS times_ordered
FROM products p
FULL JOIN order_items oi ON p.product_id = oi.product_id
GROUP BY COALESCE(p.product_id, oi.product_id), p.name;

FULL JOIN сохраняет строки из обеих таблиц. COALESCE обрабатывает NULL с обеих сторон.

Задача 7. CROSS JOIN: все возможные комбинации продукт × регион

SELECT p.name, r.region,
  COALESCE(s.total_sales, 0) AS sales
FROM products p
CROSS JOIN (SELECT DISTINCT region FROM stores) r
LEFT JOIN (
  SELECT product_id, region, SUM(amount) AS total_sales
  FROM sales JOIN stores USING (store_id)
  GROUP BY product_id, region
) s ON p.product_id = s.product_id AND r.region = s.region;

CROSS JOIN создаёт «скелет» — все комбинации. LEFT JOIN подтягивает данные. Пустые ячейки = 0.

Задача 8. Разница ON vs WHERE с LEFT JOIN

-- Правильно: фильтр в ON
SELECT u.user_id, COUNT(o.order_id) AS recent_orders
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
  AND o.created_at >= '2026-01-01'
GROUP BY u.user_id;

-- Неправильно: фильтр в WHERE (теряет пользователей без заказов)
SELECT u.user_id, COUNT(o.order_id) AS recent_orders
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE o.created_at >= '2026-01-01' OR o.created_at IS NULL
GROUP BY u.user_id;

Задача 9. JOIN трёх таблиц: выручка по категориям и месяцам

SELECT
  p.category,
  DATE_TRUNC('month', o.created_at) AS month,
  SUM(oi.quantity * oi.price) AS revenue
FROM order_items oi
JOIN orders o ON oi.order_id = o.order_id
JOIN products p ON oi.product_id = p.product_id
GROUP BY p.category, DATE_TRUNC('month', o.created_at)
ORDER BY month, revenue DESC;

Три таблицы — минимум для реальной аналитической задачи. Порядок JOIN не влияет на результат, но влияет на читаемость.

Задача 10. LATERAL JOIN: топ-3 заказа каждого пользователя

SELECT u.user_id, u.name, t.order_id, t.amount
FROM users u,
LATERAL (
  SELECT order_id, amount
  FROM orders o
  WHERE o.user_id = u.user_id
  ORDER BY amount DESC
  LIMIT 3
) t;

LATERAL позволяет использовать столбцы из внешней таблицы в подзапросе. Альтернатива ROW_NUMBER, но иногда быстрее.


Как тренироваться

JOIN — тема, которую невозможно выучить по шпаргалке один раз и забыть. Нужна практика: писать запросы руками, видеть результаты, разбирать ошибки. Типичная ловушка — знать теорию, но на собеседовании спутать LEFT и INNER JOIN в условии с NULL.

Тренажёр Карьерник включает задачи на все виды JOIN с разборами: от базовых до задач на SELF JOIN и множественные соединения. Можно тренироваться по 15 минут в день в Telegram — этого достаточно, чтобы за неделю перестать путаться в JOIN на собеседовании.

Больше материалов по SQL — в разделе подготовки. Если хотите разобрать продвинутые темы, посмотрите шпаргалку по оконным функциям.

FAQ

Какой JOIN используется чаще всего в работе аналитика?

LEFT JOIN — с большим отрывом. В аналитических запросах почти всегда нужно сохранить все записи основной таблицы и подтянуть дополнительные данные. INNER JOIN на втором месте — для случаев, когда данные гарантированно консистентны и потеря строк невозможна. CROSS JOIN и FULL JOIN встречаются редко, но знать их нужно.

Влияет ли порядок таблиц в JOIN на производительность?

Оптимизатор большинства современных СУБД (PostgreSQL, BigQuery, SQL Server) сам выбирает порядок соединения, поэтому порядок в запросе обычно не влияет на скорость. Но он влияет на читаемость: начинайте с основной таблицы и подтягивайте справочники — так запрос проще понять и отладить. В MySQL и ClickHouse порядок может иметь значение для плана выполнения.

Чем JOIN ON отличается от JOIN USING?

JOIN ... USING(column) — сокращённый синтаксис для случая, когда столбец соединения называется одинаково в обеих таблицах. JOIN departments USING(department_id) эквивалентен JOIN departments ON employees.department_id = departments.department_id. Разница: при USING столбец появляется в результате один раз, при ON — два раза (из каждой таблицы). На собеседованиях USING почти не спрашивают, но знать полезно.