Получить доступ

JOIN, ключи и ограничения

INNER/OUTER/CROSS JOIN, ловушка ON vs WHERE, первичные/внешние/уникальные ключи и ссылочная целостность.

Что такое JOIN

JOIN соединяет строки двух таблиц по условию (ON). Концептуально база перебирает пары строк и оставляет те, что удовлетворяют условию, а тип JOIN определяет, что делать с непарными строками.

Как именно выполняется соединение (nested loop, hash join, merge join) выбирает планировщик по объёму данных и индексам — это разные алгоритмы с разной стоимостью.

Виды JOIN

INNER JOIN оставляет только совпавшие пары. LEFT (OUTER) JOIN сохраняет все строки левой таблицы, подставляя NULL там, где справа нет пары; RIGHT JOIN — зеркально. FULL OUTER JOIN сохраняет непарные строки с обеих сторон.

CROSS JOIN даёт декартово произведение (каждая с каждой) — иногда нужно, но чаще это случайно забытое условие соединения.

-- все пользователи и их заказы; пользователи без заказов тоже попадут (с NULL)
select u.id, o.id
from users u
left join orders o on o.user_id = u.id;
Четыре вида JOIN как диаграммы Венна двух таблиц: INNER — пересечение, LEFT — вся левая плюс пересечение, RIGHT — вся правая плюс пересечение, FULL — обе целиком.
INNER, LEFT, RIGHT и FULL JOIN — какие строки двух таблиц попадают в результат.

ON против WHERE — ловушка OUTER JOIN

Для INNER JOIN условие можно ставить хоть в ON, хоть в WHERE — результат одинаков. Для OUTER JOIN это не так.

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

-- хотим всех users + их PAID-заказы, сохранив users без заказов
select u.id, o.id
from users u
left join orders o
  on o.user_id = u.id and o.status = 'PAID'; -- условие в ON, а не в WHERE

Ключи и ограничения

Primary key — основной идентификатор строки: уникален, не допускает NULL, обычно один на таблицу и служит целью для внешних ключей. Естественный ключ (email) против суррогатного (auto id) — частый предмет обсуждения; суррогатный стабильнее, естественный экономит join.

Unique constraint выражает бизнес-уникальность (email, номер заказа в рамках tenant), и таких ограничений может быть несколько. Foreign key связывает колонку с ключом другой таблицы и обеспечивает ссылочную целостность.

Составной ключ (PK или unique по нескольким колонкам) уместен, когда строку идентифицирует комбинация значений, например (tenant_id, order_no).

Ссылочная целостность и индексы под FK

Foreign key не даёт создать ссылку на несуществующую строку и контролирует поведение при удалении/обновлении родителя (CASCADE, RESTRICT, SET NULL).

Важная деталь: внешний ключ часто стоит проиндексировать вручную. PK индексируется автоматически, а вот столбец-источник FK — нет, и без индекса каскадные удаления и join по нему становятся дорогими.

Три алгоритма соединения

Тип JOIN (INNER/LEFT) — это СЕМАНТИКА, какие строки попадут в результат. Алгоритм соединения — это РЕАЛИЗАЦИЯ, выбираемая планировщиком; путать их на собеседовании не стоит.

Nested loop: для каждой строки внешней таблицы ищем пары во внутренней. Дёшев, когда внешняя выборка мала, а внутренняя имеет индекс по колонке соединения — тогда каждый поиск это спуск по B-tree. Без индекса вырождается в квадратичный перебор.

Hash join: по меньшей таблице строится хеш-таблица в памяти, затем по ней проверяются строки большей. Хорош для больших наборов без индексов, но работает только с равенством (=) и требует памяти; при нехватке уходит на диск.

Merge join: обе стороны сортируются по ключу соединения и проходятся параллельно, как слияние двух отсортированных списков. Выгоден, когда данные уже отсортированы (например, читаются по индексу), иначе платим за сортировку.

Практический вывод: если в плане вместо ожидаемого index nested loop оказался seq scan + hash join на миллионах строк, обычно не хватает индекса по колонке соединения (чаще всего — по внешнему ключу).

Частые ошибки

Ловушки: переносить условие правой таблицы из ON в WHERE при OUTER JOIN, путать LEFT и RIGHT, забывать индекс на FK, полагаться только на проверки в коде вместо constraints в БД.

Ещё одна: считать, что JOIN «размножает» строки по ошибке. Если после соединения строк стало больше, значит связь на самом деле один-ко-многим — и вопрос не к JOIN, а к тому, что мы хотели получить (агрегат, DISTINCT или отдельный запрос).

Как отвечать на собеседовании

Опишите JOIN через судьбу непарных строк: INNER отбрасывает, LEFT сохраняет левые с NULL, FULL — обе стороны. Это точнее, чем пересказ диаграмм Венна, и сразу выводит на семантику NULL.

Обязательно назовите ловушку ON против WHERE: условие на правую таблицу в WHERE отбрасывает NULL-строки и превращает LEFT JOIN в INNER — это любимая проверка на понимание, а не на память.

По ключам разведите роли: PK — идентификатор строки, unique — бизнес-правило (их может быть несколько), FK — ссылочная целостность плюс поведение при удалении родителя (CASCADE/RESTRICT/SET NULL). Добавьте деталь, которая отличает практика: PK индексируется автоматически, а колонку-источник FK надо индексировать самому, иначе каскады и соединения по ней дорогие.

Если спросят про производительность соединения — назовите nested loop / hash / merge и условие, при котором каждый выгоден. Завершите тезисом: критичные инварианты держим ограничениями в БД, потому что код можно обойти багом или вторым сервисом.

Вопросы по теме

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

Закрытая бета

Знать тему — половина дела

На живом собеседовании sotto слышит вопрос интервьюера и показывает короткий ответ и план. Оставьте контакт — пришлём доступ и промокод.

  • Промокод на 2 часа — сверх пробных 30 минут
  • Программа для Windows — скачать сразу, войти — через Яндекс ID или VK ID
  • Прямая связь с разработчиком: что улучшить — сделаем