Кеширует ли SQL Server вычисленные значения в запросе?

10

Каждый раз, когда я сталкиваюсь с запросами такого типа, мне всегда интересно, как SQL Server справится с этим. Если я выполню запрос любого типа, требующий вычисления, а затем использую это значение в нескольких местах, например в selectи order by, SQL Server будет рассчитывать его дважды для каждой строки или он будет кэширован? Кроме того, как это работает с пользовательскими функциями?

Примеры:

SELECT CompanyId, Count(*)
FROM Sales
ORDER BY Count(*) desc

SELECT Geom.BufferWithTolerance(@radius, 0.01, 0).STEnvelope().STPointN(1).STX, Geom.BufferWithTolerance(@radius, 0.01, 0).STEnvelope().STPointN(1).STY
FROM Table

SELECT Id, udf.MyFunction(Id)
FROM Table
ORDER BY udf.MyFunction(Id)

Есть ли способ сделать его более эффективным или SQL Server достаточно умен, чтобы справиться с этим для меня?

Йонас Ставски
источник
«это зависит» вот одна выставка rextester.com/DXOB90032
Мартин Смит
Что вы можете сравнить с rextester.com/ARSO25902
Мартин Смит
@MartinSmith вы не используете недетерминированную функцию? Если это так, то я ожидаю, что SQL выполнит его дважды.
Джонас
всегда есть исключение! Вы можете попробовать SELECT RAND() FROM Sales order by RAND()- это оценивается только один раз, так как оно является как недетерминированным, так и постоянной времени выполнения.
Мартин Смит

Ответы:

11

Оптимизатор запросов 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 раз:

План запроса 1

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 раз:

план запроса 2

Результаты 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;

Оптимизатор запросов недостаточно умен, чтобы объединять их, поэтому будет проделана дополнительная работа:

хэш-матч 2

Джо Оббиш
источник