O SQL Server armazena em cache os valores calculados em uma consulta?

10

Sempre que me deparo com esse tipo de consulta, sempre me pergunto como o SQL Server funcionaria. Se eu executar qualquer tipo de consulta que exija um cálculo e, em seguida, usar esse valor em vários locais, por exemplo, no selecte no order by, o SQL Server o calculará duas vezes para cada linha ou será armazenado em cache? Além disso, como isso funciona com as funções definidas pelo usuário?

Exemplos:

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)

Existe uma maneira de torná-lo mais eficiente ou o SQL Server é inteligente o suficiente para lidar com isso para mim?

Jonas Stawski
fonte
"depende" aqui está uma exposição rextester.com/DXOB90032
Martin Smith
Que você pode comparar com rextester.com/ARSO25902
Martin Smith
@MartinSmith você não está usando uma função não determinística? Se for, esperaria que o SQL o executasse duas vezes.
Jonas Stawski
sempre há uma exceção! Você pode tentar SELECT RAND() FROM Sales order by RAND()- isso é avaliado apenas uma vez, pois é não determinístico e constante de tempo de execução.
Martin Smith

Respostas:

11

O otimizador de consulta do SQL Server pode combinar valores calculados repetidos em um único operador do Compute Scalar. A decisão de fazer ou não isso depende do custo do plano de consulta e das propriedades do valor calculado. Como esperado, ele não fará isso para valores calculados que são não determinísticos, e algumas exceções, como RAND(). Também não fará isso para funções definidas pelo usuário.

Começarei com um exemplo de função definida pelo usuário. Aqui está um excelente exemplo de uma função definida pelo usuário:

CREATE OR ALTER FUNCTION dbo.NULL_FUNCTION (@N BIGINT) RETURNS BIGINT
WITH SCHEMABINDING
AS
BEGIN
RETURN NULL;
END;

Também quero criar uma tabela e colocar 100 linhas nela:

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;

A dbo.NULL_FUNCTIONfunção é determinista. Quantas vezes será executado para a seguinte consulta?

SELECT n, dbo.NULL_FUNCTION(n)
FROM X_100;

Com base no plano de consulta, isso será executado uma vez para cada linha ou 100 vezes:

plano de consulta 1

O SQL Server 2016 apresentou a DMV sys.dm_exec_function_stats . Podemos tirar instantâneos dessa DMV para ver quantas vezes um UDF é executado por uma consulta.

SELECT execution_count
FROM sys.dm_exec_function_stats
WHERE object_id = OBJECT_ID('NULL_FUNCTION');

O resultado é 100, então a função foi executada 100 vezes.

Vamos tentar outra consulta simples:

SELECT n, dbo.NULL_FUNCTION(n), dbo.NULL_FUNCTION(n) 
FROM X_100;

O plano de consulta sugere que a função seja executada 200 vezes:

plano de consulta 2

Os resultados sys.dm_exec_function_statssugerem que a função foi executada 200 vezes.

Observe que você nem sempre pode usar o plano de consulta para descobrir quantas vezes um escalar de computação é executado. A citação a seguir é de " Escalares de computação, expressões e desempenho do plano de execução ":

Isso leva as pessoas a pensar que o Compute Scalar se comporta como a maioria dos outros operadores: conforme as linhas fluem por ele, os resultados de quaisquer cálculos que o Compute Scalar contém são adicionados ao fluxo. Isso geralmente não é verdade. Apesar do nome, o Compute Scalar nem sempre calcula nada e nem sempre contém um único valor escalar (pode ser um vetor, um alias ou até um predicado booleano, por exemplo). Mais frequentemente, um Escalar de computação simplesmente define uma expressão; o cálculo real é adiado até que algo mais tarde no plano de execução precise do resultado.

Vamos tentar outro exemplo. Para a seguinte consulta, espero que o UDF seja calculado uma vez:

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;

O plano de consulta sugere que será calculado uma vez:

plano de consulta

No entanto, o DMV revela a verdade. O escalar de computação é adiado até que seja necessário, que está no operador de junção. É avaliado 100 vezes.

Você também perguntou o que pode fazer para incentivar o otimizador a evitar recalcular a mesma expressão várias vezes. A melhor coisa que você pode fazer é evitar o uso de UDFs escalares no seu código. Eles têm vários problemas de desempenho fora dessa questão, incluindo inflar concessões de memória, forçando a execução de toda a consulta MAXDOP 1, estimativas ruins de cardinalidade e levando a utilização adicional da CPU. Se você precisar usar um UDF e o valor desse UDF for uma constante, poderá calculá-lo fora da consulta e colocá-lo em uma variável local.

Para consultas sem UDFs, você pode evitar escrever expressões que retornam o mesmo resultado, mas não são digitadas exatamente da mesma maneira. Neste próximo exemplo, estou usando o banco de dados AdventureworksDW2016CTP3 disponível publicamente, mas qualquer banco de dados serve. Quantas vezes serão COUNT(*)calculadas para esta consulta?

SELECT OrderDateKey, COUNT(*) 
FROM dbo.FactResellerSales
GROUP BY OrderDateKey
ORDER BY COUNT(*) DESC;

Para esta consulta, podemos descobrir isso observando o operador Hash Match (agregate).

combinação de hash

O COUNT(*)é calculado uma vez para cada valor exclusivo de OrderDateKey. A inclusão da ORDER BYcláusula não faz com que seja calculada duas vezes. Você pode ver o plano de execução aqui .

Agora, considere uma consulta que retornará exatamente os mesmos resultados, mas que seja escrita de uma maneira diferente:

SELECT OrderDateKey, SUM(1)
FROM dbo.FactResellerSales
GROUP BY OrderDateKey
ORDER BY COUNT(*) DESC;

O otimizador de consulta não é inteligente o suficiente para combiná-los; portanto, trabalho adicional será feito:

combinação de hash 2

Joe Obbish
fonte