SQLJOINсобеседованиеоптимизация

SQL JOIN — все типы с примерами и подводные камни

2026-04-25 13 мин

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) — тоже не попадёт.

Слово INNER можно опустить
Просто 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;
CROSS JOIN — главная причина «зависших» запросов
Случайный CROSS JOIN двух таблиц по 100K строк = 10 млрд строк. Уберёт сервер. Если в FROM перечислил две таблицы через запятую и забыл WHERE — это неявный CROSS JOIN. Всегда пиши JOIN явно.


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 делает две вещи:

Удобно когда таблицы хорошо именованы. 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-тренажёр, найди задачу с двумя таблицами — попробуй сначала INNER, потом LEFT. Сравни количество строк. Понять разницу за 5 минут на реальной задаче лучше любого учебника.

Реши задачу на JOIN сейчас
325 SQL-задач с проверкой. INNER, LEFT, оконные, CTE — на реальных схемах. Первые 5 бесплатно.
Открыть SQL-тренажёр →