Как выполняются JOIN операции в базах данных
Что проверяют
Знаете ли вы, что за словом JOIN стоят два уровня: логический (какие строки попадут в результат) и физический (каким алгоритмом база их соединит). От C#-разработчика ждут ещё и понимания, какой SQL генерирует Entity Framework Core. Следом часто просят прочитать план запроса или объяснить, почему Include вдруг стал медленным.
Как отвечать
Логические виды назовите коротко: INNER, LEFT и RIGHT, FULL OUTER, CROSS, а для SQL Server ещё CROSS APPLY и OUTER APPLY, которые соединяют строку с результатом подзапроса. Строки с NULL в ключе не совпадают ни с чем, даже с другим NULL.
Главное в ответе это физические операторы из плана выполнения:
- Nested Loops: для каждой строки внешнего набора ищет пары во внутреннем. Хорош, когда внешний набор маленький, а по внутреннему есть индекс;
- Hash Match: строит хэш-таблицу по меньшему входу и прогоняет через неё больший. Подходит для больших несортированных наборов, но требует памяти и при её нехватке сбрасывает данные в
tempdb; - Merge Join: идёт по двум входам, отсортированным по ключу соединения, за один проход. Очень дёшев, если сортировку дают индексы;
- Adaptive Join: в новых версиях SQL Server выбирает между хэшем и вложенными циклами уже во время выполнения, по фактическому числу строк.
Выбор делает оптимизатор по статистике, поэтому устаревшая статистика и неверная оценка числа строк чаще всего и дают плохой план. В EF Core Include по коллекции превращается в LEFT JOIN, а для нескольких коллекций имеет смысл AsSplitQuery(). В LINQ левое соединение традиционно пишется через GroupJoin и DefaultIfEmpty(), а в .NET 10 для этого появился LeftJoin.
Где спотыкаются
Перечисляют только логические типы и не могут сказать, что происходит внутри. Вопрос «как выполняются» именно про алгоритмы.
Кладут условие на правую таблицу в WHERE после LEFT JOIN. Строки с NULL отсекаются, и соединение становится внутренним; условие нужно переносить в ON.
Подключают в EF Core несколько Include по коллекциям в одном запросе. Получается декартов взрыв: каждая строка одной коллекции умножается на каждую строку другой, и объём данных растёт на порядки.