SQL Server met-il en cache les valeurs calculées dans une requête?

10

Chaque fois que je rencontre ce type de requêtes, je me demande toujours comment SQL Server fonctionnerait. Si j'exécute un type de requête qui nécessite un calcul et que j'utilise ensuite cette valeur à plusieurs endroits, par exemple dans le selectet le order by, SQL Server le calculera-t-il deux fois pour chaque ligne ou sera-t-il mis en cache? De plus, comment cela fonctionne-t-il avec les fonctions définies par l'utilisateur?

Exemples:

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-t-il un moyen de le rendre plus efficace ou SQL Server est-il suffisamment intelligent pour le gérer pour moi?

Jonas Stawski
la source
"ça dépend", voici une exposition rextester.com/DXOB90032
Martin Smith
Que vous pouvez comparer avec rextester.com/ARSO25902
Martin Smith
@MartinSmith n'utilisez-vous pas une fonction non déterministe? Si c'est le cas, je m'attendrais à ce que SQL l'exécute deux fois.
Jonas Stawski
il y a toujours une exception! Vous pouvez essayer SELECT RAND() FROM Sales order by RAND()- ceci n'est évalué qu'une seule fois car il est à la fois non déterministe et une constante de temps d'exécution.
Martin Smith

Réponses:

11

L'optimiseur de requêtes SQL Server peut combiner des valeurs calculées répétées en un seul opérateur de calcul scalaire. Le fait de le faire ou non dépend du coût du plan de requête et des propriétés de la valeur calculée. Comme prévu, il ne le fera pas pour les valeurs calculées qui ne sont pas déterministes, à quelques exceptions près comme RAND(). Il ne le fera pas non plus pour les fonctions définies par l'utilisateur.

Je vais commencer par un exemple de fonction définie par l'utilisateur. Voici un excellent exemple de fonction définie par l'utilisateur:

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

Je veux aussi créer un tableau et y mettre 100 lignes:

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_FUNCTIONfonction est déterministe. Combien de fois sera-t-il exécuté pour la requête suivante?

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

En fonction du plan de requête, cela sera exécuté une fois pour chaque ligne, ou 100 fois:

plan de requête 1

SQL Server 2016 a introduit le DMV sys.dm_exec_function_stats . Nous pouvons prendre des instantanés de ce DMV pour voir combien de fois un UDF est exécuté par une requête.

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

Le résultat est 100, donc la fonction a été exécutée 100 fois.

Essayons une autre requête simple:

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

Le plan de requête suggère que la fonction sera exécutée 200 fois:

plan de requête 2

Les résultats de sys.dm_exec_function_statssuggèrent que la fonction a été exécutée 200 fois.

Notez que vous ne pouvez pas toujours utiliser le plan de requête pour déterminer combien de fois un scalaire de calcul est exécuté. La citation suivante est extraite de " Calculer les scalaires, les expressions et les performances du plan d'exécution ":

Cela amène les gens à penser que Compute Scalar se comporte comme la majorité des autres opérateurs: à mesure que les lignes le traversent, les résultats des calculs que contient Compute Scalar sont ajoutés au flux. Ce n'est généralement pas vrai. Malgré son nom, Compute Scalar ne calcule pas toujours quoi que ce soit et ne contient pas toujours une seule valeur scalaire (il peut s'agir d'un vecteur, d'un alias ou même d'un prédicat booléen, par exemple). Le plus souvent, un calcul scalaire définit simplement une expression; le calcul réel est différé jusqu'à ce que quelque chose plus tard dans le plan d'exécution ait besoin du résultat.

Essayons un autre exemple. Pour la requête suivante, j'espère que l'UDF est calculé une fois:

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;

Le plan de requête suggère qu'il sera calculé une seule fois:

plan de requête

Cependant, le DMV révèle la vérité. Le scalaire de calcul est différé jusqu'à ce qu'il soit nécessaire, ce qui se trouve dans l'opérateur de jointure. Il est évalué 100 fois.

Vous avez également demandé ce que vous pouvez faire pour encourager l'optimiseur à éviter de recalculer plusieurs fois la même expression. La meilleure chose que vous puissiez faire est d'éviter d'utiliser des FDU scalaires dans votre code. Ceux-ci présentent un certain nombre de problèmes de performances en dehors de cette question, notamment le gonflement des allocations de mémoire, le forçage de la requête entière à exécuter MAXDOP 1, de mauvaises estimations de cardinalité et une utilisation supplémentaire du processeur. Si vous devez utiliser un UDF et que la valeur de cet UDF est une constante, vous pouvez le calculer en dehors de la requête et le placer dans une variable locale.

Pour les requêtes sans UDF, vous pouvez essayer d'éviter d'écrire des expressions qui retournent le même résultat mais ne sont pas tapées exactement de la même manière. Pour cet exemple suivant, j'utilise la base de données AdventureworksDW2016CTP3 accessible au public, mais vraiment n'importe quelle base de données fera l'affaire. Combien de fois sera COUNT(*)calculé pour cette requête?

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

Pour cette requête, nous pouvons comprendre cela en regardant l'opérateur Hash Match (agrégat).

match de hachage

Le COUNT(*)est calculé une fois pour chaque valeur unique de OrderDateKey. L'inclusion de la ORDER BYclause ne la fait pas être calculée deux fois. Vous pouvez voir le plan d'exécution ici .

Maintenant, considérez une requête qui retournera exactement les mêmes résultats mais qui est écrite d'une manière différente:

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

L'optimiseur de requêtes n'est pas assez intelligent pour les combiner, donc un travail supplémentaire sera effectué:

hachage match 2

Joe Obbish
la source