Как работают виды JOIN: left join, right join, outer join, inner join в SQL?
Что проверяют
Знаете ли вы, какие строки вернёт каждый вид соединения, и понимаете ли, какой SQL генерирует ваш LINQ-запрос. Для C#-разработчика следом почти всегда идёт вопрос, как сделать левое соединение в EF Core и откуда берётся «картезианский взрыв».
Как отвечать
- INNER JOIN: только пары, где условие
ONвыполнено, строки без пары отбрасываются; - LEFT JOIN: все строки левой таблицы, у несовпавших колонки правой равны
NULL; - RIGHT JOIN: то же зеркально, обычно его переписывают в
LEFT, меняя таблицы местами; - FULL OUTER JOIN: все строки обеих сторон, недостающие заполнены
NULL. SQL Server и PostgreSQL его поддерживают; - CROSS JOIN: каждая строка с каждой.
OUTER это необязательное слово: LEFT JOIN и LEFT OUTER JOIN одно и то же. Соединение возвращает строку на каждую совпавшую пару, поэтому связь один ко многим размножает строки левой таблицы.
Теперь про .NET:
Joinв LINQ: это внутреннее соединение;- левое соединение классически пишется через
GroupJoin,SelectManyиDefaultIfEmpty(). В .NET 10 появились операторыLeftJoinиRightJoin, и EF Core 10 переводит их в SQL; - навигационные свойства:
Includeпо обязательной ссылке даётINNER JOIN, по необязательной и по коллекцииLEFT JOIN. Часто join руками вообще не нужен, достаточно навигации внутриSelect; - Dapper выполняет ваш SQL как есть, вид соединения вы выбираете сами, а
splitOnраскладывает строку результата на несколько объектов.
Проверять сгенерированный SQL удобно через ToQueryString() или логирование EF Core.
Где спотыкаются
Пишут GroupJoin без DefaultIfEmpty() и получают внутреннее соединение там, где ждали левое.
Подключают несколько Include по коллекциям в одном запросе. Строки перемножаются: пять заказов и пять адресов дают двадцать пять строк на клиента. Лечится AsSplitQuery() или проекцией только нужных полей.
Фильтруют правую таблицу в WHERE после LEFT JOIN, например WHERE o.Status = 'Paid'. Строки с NULL отбрасываются, и левое соединение превращается во внутреннее. Условие переносят в ON или, в LINQ, в фильтр правой коллекции до DefaultIfEmpty().