JOIN — самая частая операция SQL. И самая частая причина медленных запросов и неправильных ответов. На собеседовании всегда дают задачу с двумя-тремя таблицами и просят что-то сосчитать. Если ошибся в типе JOIN — потерял часть данных или получил дубликаты, не заметил, отдал отчёт менеджеру, тот принял неправильное решение.
Эта статья — про все 6 типов JOIN, типичные ошибки с NULL и дубликатами, и как не положить базу при JOIN двух больших таблиц.
INNER JOIN — пересечение
Возвращает только те строки, где есть совпадение в обеих таблицах.
SELECT u.email, p.amount
FROM users u
INNER JOIN payments p ON p.user_id = u.id;
-- Возвращает только пользователей с хотя бы одной оплатой
Юзер без оплат — НЕ попадёт. Оплата без юзера (orphan) — тоже не попадёт.
JOIN = INNER JOIN. На собеседовании пиши явно INNER — показывает понимание. В продакшене для сложных запросов тоже явно — читается лучше.LEFT JOIN (LEFT OUTER) — все из левой
Возвращает ВСЕ строки из левой таблицы, плюс совпадения из правой. Если справа нет — NULL.
SELECT u.email, COALESCE(SUM(p.amount), 0) AS total_paid
FROM users u
LEFT JOIN payments p ON p.user_id = u.id
GROUP BY u.email;
-- Все юзеры, у кого нет оплат — total_paid = 0
Самый частый JOIN в аналитике. Используется когда хочешь полный список из «главной» таблицы и опциональные данные из связанной.
Подводный камень: фильтрация LEFT JOIN превращается в INNER
-- ❌ НЕПРАВИЛЬНО: WHERE убивает суть LEFT JOIN
SELECT u.email, p.amount
FROM users u
LEFT JOIN payments p ON p.user_id = u.id
WHERE p.amount > 100;
-- Юзеры без оплат отфильтровываются, потому что p.amount = NULL не > 100
-- ✅ ПРАВИЛЬНО: условие в ON, не в WHERE
SELECT u.email, p.amount
FROM users u
LEFT JOIN payments p ON p.user_id = u.id AND p.amount > 100;
Это #1 баг новичков. На собеседовании в Яндексе и Сбере специально дают такую задачу.
RIGHT JOIN — почти никто не использует
Зеркало LEFT JOIN — все строки правой таблицы. На практике используется редко: проще поменять таблицы местами и сделать LEFT.
-- Эти два запроса дают одинаковый результат
SELECT * FROM a RIGHT JOIN b ON a.id = b.a_id;
SELECT * FROM b LEFT JOIN a ON a.id = b.a_id;
Если видишь RIGHT JOIN в чужом коде — переписывай на LEFT для читаемости.
FULL OUTER JOIN — объединение
Все строки из обеих таблиц. Совпадения в обеих, NULL где нет совпадения.
-- Сравнение двух источников данных:
-- какие user_id есть только в users, какие только в analytics
SELECT
COALESCE(u.id, a.user_id) AS user_id,
u.email,
a.last_event_ts
FROM users u
FULL OUTER JOIN analytics a ON u.id = a.user_id
WHERE u.id IS NULL OR a.user_id IS NULL;
-- Только несоответствия
Используется для аудита: «всё ли совпадает между двумя таблицами». В MySQL FULL OUTER нет — эмулируется через UNION двух LEFT JOIN.
CROSS JOIN — декартово произведение
Каждая строка слева × каждая строка справа. Без условия. Используется для генерации комбинаций.
-- Календарь × все пользователи: показать активность каждого юзера в каждый день
SELECT d.day, u.id, COALESCE(events.cnt, 0) AS events_cnt
FROM days d
CROSS JOIN users u
LEFT JOIN events ON DATE(events.ts) = d.day AND events.user_id = u.id
GROUP BY d.day, u.id;
SELF JOIN — таблица сама с собой
Когда нужно сравнить строки одной таблицы между собой. Например, найти иерархию.
-- Найти manager-employee пары
SELECT
e.name AS employee,
m.name AS manager
FROM employees e
LEFT JOIN employees m ON m.id = e.manager_id;
Или сравнить значения с предыдущей строкой:
-- День, разница выручки с прошлым днём (без оконных функций)
SELECT
today.day,
today.revenue,
today.revenue - yesterday.revenue AS delta
FROM daily_revenue today
LEFT JOIN daily_revenue yesterday ON yesterday.day = today.day - INTERVAL '1 day';
Современный код пишет это через LAG() оконную — но иногда self-join чище.
ON vs USING
-- ON: явное условие
SELECT * FROM a JOIN b ON a.user_id = b.user_id;
-- USING: короче, если колонки названы одинаково
SELECT * FROM a JOIN b USING (user_id);
USING делает две вещи:
- Требует одинакового имени колонки в обеих таблицах
- В SELECT появляется ОДНА колонка
user_id(не двеa.user_idиb.user_id)
Удобно когда таблицы хорошо именованы. ClickHouse и MySQL поддерживают, PostgreSQL тоже.
Дубликаты после JOIN — вторая частая ошибка
-- users (1 строка на юзера)
-- payments (несколько строк на юзера)
SELECT u.email, p.amount
FROM users u JOIN payments p ON p.user_id = u.id;
-- Если у юзера 5 оплат — он появится 5 раз с разными amount
Если ожидается «1 строка на юзера» — заранее агрегируй:
-- ✅ ПРАВИЛЬНО
SELECT u.email, p.total
FROM users u
JOIN (
SELECT user_id, SUM(amount) AS total FROM payments GROUP BY user_id
) p ON p.user_id = u.id;
-- Или через CTE — читается лучше
Оптимизация JOIN на больших таблицах
Индексы на JOIN-колонках
CREATE INDEX idx_payments_user_id ON payments(user_id);
Без индекса — full scan правой таблицы для каждой строки левой = O(N×M). С индексом — O(N×log M).
Фильтруй ДО JOIN
-- ❌ ПЛОХО: JOIN всех строк, потом фильтрация
SELECT * FROM a JOIN b ON a.id = b.a_id WHERE a.created_at > '2026-01-01';
-- ✅ ЛУЧШЕ: фильтр в подзапросе
SELECT * FROM (SELECT * FROM a WHERE created_at > '2026-01-01') a JOIN b ON a.id = b.a_id;
-- Большинство оптимизаторов сделают это сами, но не все. CTE/subquery — гарантия.
Маленькую таблицу слева, большую справа (для hash join)
В PostgreSQL/MySQL hash join строит хэш-таблицу из правой таблицы. Если справа маленькая — экономия памяти и времени.
EXPLAIN ANALYZE — обязательно
EXPLAIN ANALYZE
SELECT u.email, COUNT(p.id)
FROM users u LEFT JOIN payments p ON p.user_id = u.id
GROUP BY u.email;
Покажет: nested loop / hash join / merge join, сколько строк, время. Если видишь nested loop на больших таблицах — добавляй индекс.
Связанные материалы
- SQL-тренажёр: задачи на JOIN — 325 задач с автопроверкой
- Оконные функции SQL — следующий уровень после JOIN
- CTE и рекурсивные запросы — как сделать сложные JOIN читаемее
- ClickHouse для аналитика — почему JOIN в OLAP — это особая боль
- AI-собеседование — потренируй задачи на JOIN с обратной связью
Открой SQL-тренажёр, найди задачу с двумя таблицами — попробуй сначала INNER, потом LEFT. Сравни количество строк. Понять разницу за 5 минут на реальной задаче лучше любого учебника.