¿SQL Server almacena en caché los valores calculados en una consulta?

10

Cada vez que me encuentro con este tipo de consultas, siempre me pregunto cómo funcionaría SQL Server. Si ejecuto cualquier tipo de consulta que requiera un cálculo y luego uso ese valor en varios lugares, por ejemplo, en selecty order by, ¿SQL Server lo calculará dos veces para cada fila o se almacenará en caché? Además, ¿cómo funciona esto con las funciones definidas por el usuario?

Ejemplos:

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)

¿Hay alguna manera de hacerlo más eficiente o SQL Server es lo suficientemente inteligente como para manejarlo por mí?

Jonas Stawski
fuente
"depende" aquí hay una exposición rextester.com/DXOB90032
Martin Smith
Que puedes comparar con rextester.com/ARSO25902
Martin Smith
@ MartinSmith, ¿no estás usando una función no determinista? Si es así, esperaría que SQL lo ejecute dos veces.
Jonas Stawski
siempre hay una excepción! Puede intentarlo SELECT RAND() FROM Sales order by RAND(): esto solo se evalúa una vez, ya que es no determinista y una constante de tiempo de ejecución.
Martin Smith

Respuestas:

11

El optimizador de consultas de SQL Server puede combinar valores calculados repetidos en un único operador Compute Scalar. Si lo hará o no depende del costo del plan de consulta y de las propiedades del valor calculado. Como se esperaba, no hará esto para valores calculados que no sean deterministas, que son algunas excepciones como RAND(). Tampoco lo hará para las funciones definidas por el usuario.

Comenzaré con un ejemplo de función definida por el usuario. Aquí hay un excelente ejemplo de una función definida por el usuario:

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

También quiero crear una tabla y poner 100 filas en ella:

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;

La dbo.NULL_FUNCTIONfunción es determinista. ¿Cuántas veces se ejecutará para la siguiente consulta?

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

Según el plan de consulta, esto se ejecutará una vez para cada fila, o 100 veces:

plan de consulta 1

SQL Server 2016 introdujo el DMV sys.dm_exec_function_stats . Podemos tomar instantáneas de ese DMV para ver cuántas veces una consulta ejecuta un UDF.

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

El resultado de eso es 100, por lo que la función se ejecutó 100 veces.

Probemos con otra consulta simple:

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

El plan de consulta sugiere que la función se ejecutará 200 veces:

plan de consulta 2

Los resultados de sys.dm_exec_function_statssugieren que la función se ejecutó 200 veces.

Tenga en cuenta que no siempre puede usar el plan de consulta para calcular cuántas veces se ejecuta un escalar de cálculo. La siguiente cita es de " Calcular escalares, expresiones y rendimiento del plan de ejecución ":

Esto lleva a las personas a pensar que Compute Scalar se comporta como la mayoría de los otros operadores: a medida que las filas fluyen a través de él, los resultados de cualquier cálculo que contenga Compute Scalar se agregan a la secuencia. Esto no es generalmente cierto. A pesar del nombre, Compute Scalar no siempre calcula nada, y no siempre contiene un solo valor escalar (puede ser un vector, un alias o incluso un predicado booleano, por ejemplo). La mayoría de las veces, un Escalar Compute simplemente define una expresión; el cálculo real se difiere hasta que algo posterior en el plan de ejecución necesite el resultado.

Probemos con otro ejemplo. Para la siguiente consulta, espero que el UDF se calcule una 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;

El plan de consulta sugiere que se calculará una vez:

plan de consulta

Sin embargo, el DMV revela la verdad. El escalar de cálculo se difiere hasta que se necesita, que está en el operador de unión. Se evalúa 100 veces.

También preguntó qué puede hacer para alentar al optimizador a evitar volver a calcular la misma expresión varias veces. Lo mejor que puede hacer es evitar el uso de UDF escalares en su código. Esos tienen una serie de problemas de rendimiento fuera de esta pregunta, que incluyen inflar las concesiones de memoria, obligar a que se ejecute toda la consulta MAXDOP 1, estimaciones de cardinalidad incorrecta y conducir a una utilización adicional de la CPU. Si necesita usar un UDF y el valor de ese UDF es una constante, puede calcularlo fuera de la consulta y colocarlo en una variable local.

Para consultas sin UDF, puede intentar evitar escribir expresiones que devuelvan el mismo resultado pero que no se escriban exactamente de la misma manera. Para este próximo ejemplo, estoy usando la base de datos AdventureworksDW2016CTP3 disponible públicamente, pero realmente cualquier base de datos funcionará. ¿Cuántas veces se COUNT(*)calculará para esta consulta?

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

Para esta consulta, podemos resolver esto mirando el operador Hash Match (agregado).

hash match

El COUNT(*)se calcula una vez para cada valor único de OrderDateKey. La inclusión de la ORDER BYcláusula no hace que se calcule dos veces. Puedes ver el plan de ejecución aquí .

Ahora, considere una consulta que devolverá exactamente los mismos resultados pero que está escrita de manera diferente:

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

El optimizador de consultas no es lo suficientemente inteligente como para combinarlos, por lo que se realizará un trabajo adicional:

hash match 2

Joe Obbish
fuente