Я не уверен, если это действительно ответит на ваш вопрос ...
Короче говоря: любой тип JOIN создаст два набора результатов и сопоставит их с заданным условием, в то время как любой тип APPLY вызовет операцию строка за строкой . Если APPLY возвращает более одной строки, набор результатов добавляется (аналогично JOIN), в то время как с результатами одной строки движок просто добавляет столбцы.
Реальность будет намного более сложный.
Движок очень умен и выберет лучший план после проверки статистики, индексов, существующих планов и так далее. Весьма вероятно, что реальный план, который вы получаете, не соответствует ожиданиям. И вполне вероятно, что план, который вы получаете, может быть одинаковым для, казалось бы, разных запросов.
Попробуйте выполнить следующее с включенным «включить фактические планы»:
USE master;
GO
CREATE DATABASE testPlan;
GO
USE testPlan;
GO
CREATE TABLE t1 (ID INT IDENTITY CONSTRAINT pk PRIMARY KEY, SomeValue VARCHAR(100));
INSERT INTO t1 VALUES('MaxVal will be 100'),('MaxVal will be 200'),('MaxVal will be 300');
GO
CREATE TABLE t2(fkID INT CONSTRAINT fk FOREIGN KEY REFERENCES t1(ID),TheValue INT);
INSERT INTO t2 VALUES(1,1),(1,2),(1,100)
,(2,1),(2,2),(2,200)
,(3,1),(3,2),(3,300);
GO
--a scalar computation using MAX()
SELECT *
,(SELECT MAX(t2.TheValue) FROM t2 WHERE t1.ID=t2.fkID) AS MaxVal
FROM t1
--the same as above, but with APPLY
SELECT *
FROM t1
CROSS APPLY(SELECT MAX(t2.TheValue) FROM t2 WHERE t1.ID=t2.fkID) A(MaxVal)
--Now we pick the TOP 1 after an ORDER BY
SELECT *
,(SELECT TOP 1 t2.TheValue FROM t2 WHERE t1.ID=t2.fkID ORDER BY t2.TheValue DESC) AS MaxVal
FROM t1
--and again the same with APPLY
SELECT *
FROM t1
CROSS APPLY(SELECT TOP 1 t2.TheValue FROM t2 WHERE t1.ID=t2.fkID ORDER BY t2.TheValue DESC) A(MaxVal)
--Tim's approach using the very slick TOP 1 WITH TIES approach
SELECT TOP 1 WITH TIES *
FROM t1 INNER JOIN t2 ON t1.ID=t2.fkID
ORDER BY ROW_NUMBER() OVER(PARTITION BY t1.ID ORDER BY t2.TheValue DESC);
GO
USE master;
GO
--carefull with real data!
--DROP DATABASE testPlan;
GO
План для «scalar MAX» использует сканирование таблицы на 27 (!) строках, уменьшенных до 9. Тот же подход с APPLY имеет тот же план. Двигатель достаточно умен, чтобы видеть, что для этого не понадобится полный набор результатов. Примечание: вы можете использовать MaxVal как переменную в запросе, очень полезно ...
План с TOP 1
в подзапросе является самым дорогим в этом крошечном тесте. Он начинается с того же, что и выше (сканирование таблицы с 27 строками, уменьшено до 9), но должен добавить операцию сортировки. Вариант с APPLY примерно такой же.
Подход с TOP 1 WITH TIES
занимает 9 строк t2 и сортирует их. Следующая операция выполняется против 9 строк. Еще одна сортировка и приведение к ТОП-строкам.
В этом случае первый самый быстрый - на сегодняшний день.
Но в (вашей) реальности фактическое поведение будет зависеть от существующих индексов, статистика и фактическое количество строк. Кроме того, у вас есть один дополнительный уровень (еще одна таблица) между ними. Чем сложнее запрос, тем сложнее оптимизатору будет найти лучший план.
Заключение
Если производительность имеет значение, то мчится на лошадях и делает измерения. Если производительность не так важна, возьмите запрос, который легче читать, понимать и обслуживать.