Что такое CROSS APPLY и чем он отличается от JOIN в SQL Server?
Что проверяют
Понимаете ли вы главное отличие: правая часть APPLY видит столбцы текущей строки слева, а правая часть JOIN нет. Для C#-разработчика следом часто идёт практика: задача «последние N записей на группу», разница OUTER APPLY и LEFT JOIN и то, откуда APPLY берётся в SQL, который генерирует EF Core.
Как отвечать
JOIN: соединяет два независимых набора строк по условию вON. Подзапрос справа не может сослаться на столбец таблицы слева, вJOIN (SELECT TOP 1 ... WHERE o.CustomerId = c.Id)столбецc.Idпросто не виден;CROSS APPLY: для каждой строки слева вычисляет табличное выражение справа, и в нём можно использовать значения этой строки. Если справа пусто, строка слева отбрасывается, как приINNER JOIN;OUTER APPLY: та же логика, но строка слева остаётся, а столбцы справа заполняютсяNULL, аналогLEFT JOIN.
Справа может стоять не только табличная функция, а любое табличное выражение: подзапрос с TOP и ORDER BY, VALUES, OPENJSON, STRING_SPLIT. Типовые задачи: три последних заказа каждого клиента через CROSS APPLY (SELECT TOP 3 ... ORDER BY o.CreatedAt DESC) и разбор JSON-колонки через CROSS APPLY OPENJSON(c.Tags).
Простой коррелированный APPLY оптимизатор нередко сводит к тому же плану, что и JOIN. Для top-N при индексе (CustomerId, CreatedAt DESC) он даёт Nested Loops с поиском по индексу и часто обгоняет ROW_NUMBER(), когда групп мало, а строк в группе много.
В EF Core APPLY может появиться в проекциях с Take или FirstOrDefault по навигации. Какой SQL уходит в базу, видно через ToQueryString().
Где спотыкаются
Повторяют, что APPLY работает только с табличными функциями. На деле самый частый случай это коррелированный подзапрос.
Приравнивают CROSS APPLY к CROSS JOIN. Декартово произведение получится, только если правая часть не ссылается на левую, а вся ценность APPLY как раз в этой ссылке.
Берут CROSS APPLY там, где нужен OUTER, и молча теряют клиентов без заказов. Ещё одна ловушка для C#: тесты на SQLite. Там APPLY нет, и запрос, который EF Core для SQL Server транслирует в APPLY, на SQLite падает с ошибкой трансляции.