Оптимизатор запросов SQL Server может объединять повторяющиеся вычисленные значения в один оператор Compute Scalar. Будет ли это сделано, зависит от стоимости плана запроса и свойств рассчитанного значения. Как и ожидалось, он не будет делать это для вычисленных значений, которые являются недетерминированными, что, за исключением нескольких, таких как RAND(). Это также не будет делать это для пользовательских функций.
Я начну с примера пользовательской функции. Вот отличный пример пользовательской функции:
CREATE OR ALTER FUNCTION dbo.NULL_FUNCTION (@N BIGINT) RETURNS BIGINT
WITH SCHEMABINDING
AS
BEGIN
RETURN NULL;
END;
Я также хочу создать таблицу и поместить в нее 100 строк:
CREATE TABLE X_100 (N BIGINT NOT NULL);
WITH
L0 AS(SELECT 1 AS c UNION ALL SELECT 1),
L1 AS(SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B),
L2 AS(SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B),
L3 AS(SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B),
L4 AS(SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B),
L5 AS(SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B),
Nums AS(SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS n FROM L5)
INSERT INTO X_100 WITH (TABLOCK)
SELECT n
FROM Nums WHERE n <= 100;
dbo.NULL_FUNCTIONФункция determistic. Сколько раз он будет выполнен для следующего запроса?
SELECT n, dbo.NULL_FUNCTION(n)
FROM X_100;
На основании плана запроса это будет выполнено один раз для каждой строки или 100 раз:

SQL Server 2016 представил DMV sys.dm_exec_function_stats . Мы можем сделать снимки этого DMV, чтобы увидеть, сколько раз UDF выполняется запросом.
SELECT execution_count
FROM sys.dm_exec_function_stats
WHERE object_id = OBJECT_ID('NULL_FUNCTION');
Результат равен 100, поэтому функция была выполнена 100 раз.
Давайте попробуем еще один простой запрос:
SELECT n, dbo.NULL_FUNCTION(n), dbo.NULL_FUNCTION(n)
FROM X_100;
План запроса предполагает, что функция будет выполнена 200 раз:

Результаты sys.dm_exec_function_statsсвидетельствуют о том, что функция была выполнена 200 раз.
Обратите внимание, что вы не всегда можете использовать план запроса, чтобы выяснить, сколько раз выполняется вычислительный скаляр. Следующая цитата из " Вычислить скаляры, выражения и производительность плана выполнения ":
Это заставляет людей думать, что Compute Scalar ведет себя так же, как и большинство других операторов: когда через него проходят строки, результаты любых вычислений, которые содержит Compute Scalar, добавляются в поток. Это не совсем так. Несмотря на название, Compute Scalar не всегда вычисляет что-либо и не всегда содержит одно скалярное значение (например, это может быть вектор, псевдоним или даже логический предикат). Чаще всего Compute Scalar просто определяет выражение; фактическое вычисление откладывается до тех пор, пока что-то позже в плане выполнения не потребует результата.
Давайте попробуем другой пример. Для следующего запроса я надеюсь, что UDF рассчитывается один раз:
WITH NULL_FUNCTION_CTE (NULL_VALUE) AS
(
SELECT DISTINCT dbo.NULL_FUNCTION(0)
)
SELECT n , cte.NULL_VALUE
FROM X_100
CROSS JOIN NULL_FUNCTION_CTE cte;
План запроса предполагает, что он будет рассчитан один раз:

Тем не менее, DMV раскрывает правду. Вычислительный скаляр откладывается до тех пор, пока он не понадобится, что находится в операторе соединения. Это оценивается 100 раз.
Вы также спросили, что вы можете сделать, чтобы оптимизатор избегал повторного вычисления одного и того же выражения несколько раз. Лучшее, что вы можете сделать, это избегать использования скалярных UDF в вашем коде. У них есть ряд проблем с производительностью, выходящих за рамки этого вопроса, в том числе раздувание выделений памяти, принудительное выполнение всего запроса MAXDOP 1, неверные оценки мощности и ведение к дополнительной загрузке ЦП. Если вам нужно использовать UDF и значение этого UDF является константой, вы можете вычислить его вне запроса и поместить в локальную переменную.
Для запросов без UDF вы можете попытаться избежать написания выражений, которые возвращают один и тот же результат, но не набираются одинаково. Для этого следующего примера я использую общедоступную базу данных AdventureworksDW2016CTP3, но на самом деле подойдет любая база данных. Сколько раз будет COUNT(*)рассчитано для этого запроса?
SELECT OrderDateKey, COUNT(*)
FROM dbo.FactResellerSales
GROUP BY OrderDateKey
ORDER BY COUNT(*) DESC;
Для этого запроса мы можем выяснить это, посмотрев на оператор Hash Match (aggregate).

COUNT(*)Вычисляется один раз для каждого уникального значения OrderDateKey. Включение ORDER BYпункта не приводит к тому, что он рассчитывается дважды. Вы можете увидеть план выполнения здесь .
Теперь рассмотрим запрос, который вернет точно такие же результаты, но написан по-другому:
SELECT OrderDateKey, SUM(1)
FROM dbo.FactResellerSales
GROUP BY OrderDateKey
ORDER BY COUNT(*) DESC;
Оптимизатор запросов недостаточно умен, чтобы объединять их, поэтому будет проделана дополнительная работа:
