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 почти не спрашивают, но знать полезно.