web-dev-qa-db-fra.com

Pourquoi ce tableau dérivé améliore-t-il les performances?

J'ai une requête qui prend une chaîne json comme paramètre. Le json est un tableau de paires de latitude et longitude. Un exemple d'entrée peut être le suivant.

declare @json nvarchar(max)= N'[[40.7592024,-73.9771259],[40.7126492,-74.0120867]
,[41.8662374,-87.6908788],[37.784873,-122.4056546]]';

Il appelle un TVF qui calcule le nombre de POI autour d'un point géographique, à des distances de 1,3,5,10 mile.

create or alter function [dbo].[fn_poi_in_dist](@geo geography)
returns table
with schemabinding as
return 
select count_1  = sum(iif(LatLong.STDistance(@geo) <= 1609.344e * 1,1,0e))
      ,count_3  = sum(iif(LatLong.STDistance(@geo) <= 1609.344e * 3,1,0e))
      ,count_5  = sum(iif(LatLong.STDistance(@geo) <= 1609.344e * 5,1,0e))
      ,count_10 = count(*)
from dbo.point_of_interest
where LatLong.STDistance(@geo) <= 1609.344e * 10

L'intention de la requête json est d'appeler cette fonction en bloc. Si je l'appelle comme ça, la performance est très mauvaise en prenant près de 10 secondes pour seulement 4 points:

select row=[key]
      ,count_1
      ,count_3
      ,count_5
      ,count_10
from openjson(@json)
cross apply dbo.fn_poi_in_dist(
            geography::Point(
                convert(float,json_value(value,'$[0]'))
               ,convert(float,json_value(value,'$[1]'))
               ,4326))

plan = https://www.brentozar.com/pastetheplan/?id=HJDCYd_o4

Cependant, le déplacement de la construction de la géographie à l'intérieur d'une table dérivée entraîne une amélioration spectaculaire des performances, ce qui termine la requête en environ 1 seconde.

select row=[key]
      ,count_1
      ,count_3
      ,count_5
      ,count_10
from (
select [key]
      ,geo = geography::Point(
                convert(float,json_value(value,'$[0]'))
               ,convert(float,json_value(value,'$[1]'))
               ,4326)
from openjson(@json)
) a
cross apply dbo.fn_poi_in_dist(geo)

plan = https://www.brentozar.com/pastetheplan/?id=HkSS5_OoE

Les plans semblent pratiquement identiques. Aucun n'utilise le parallélisme et les deux utilisent l'index spatial. Il y a une bobine différée supplémentaire sur le plan lent que je peux éliminer avec l'indice option(no_performance_spool). Mais les performances de la requête ne changent pas. Il reste encore beaucoup plus lent.

L'exécution des deux avec l'indication ajoutée dans un lot pèsera également les deux requêtes.

Version du serveur SQL = Microsoft SQL Server 2016 (SP1-CU7-GDR) (KB4057119) - 13.0.4466.4 (X64)

Donc ma question est pourquoi est-ce important? Comment savoir si je dois calculer des valeurs dans une table dérivée ou non?

18
Michael B

Je peux vous donner une réponse partielle qui explique pourquoi vous voyez la différence de performances - bien que cela laisse encore des questions ouvertes (telles que peut SQL Server produire le plan plus optimal sans introduire une expression de table intermédiaire qui projette l'expression comme une colonne?)


La différence est que dans le plan rapide, le travail nécessaire pour analyser les éléments du tableau JSON et créer la géographie est effectué 4 fois (une fois pour chaque ligne émise par la fonction openjson) - alors qu'il est effectué plus de 100 000 = fois cela dans le plan lent.

Dans le plan rapide ...

geography::Point(
                convert(float,json_value(value,'$[0]'))
               ,convert(float,json_value(value,'$[1]'))
               ,4326)

Est affecté à Expr1000 dans le scalaire de calcul à gauche de la fonction openjson. Cela correspond à geo dans votre définition de table dérivée.

enter image description here

Dans le plan rapide, le filtre et le flux regroupent la référence Expr1000. Dans le plan lent, ils font référence à l'expression sous-jacente complète.

Propriétés d'agrégat de flux

enter image description here

Le filtre est exécuté 116 995 fois, chaque exécution nécessitant une évaluation d'expression. L'agrégat de flux comporte 110 520 lignes qui y circulent pour l'agrégation et crée trois agrégats distincts à l'aide de cette expression. 110,520 * 3 + 116,995 = 448,555. Même si chaque évaluation individuelle prend 18 microsecondes, cela ajoute jusqu'à 8 secondes de temps supplémentaire pour la requête dans son ensemble.

Vous pouvez voir l'effet de cela dans les statistiques de temps réelles dans le plan XML (annoté en rouge ci-dessous du plan lent et en bleu pour le plan rapide - les temps sont en ms)

enter image description here

L'agrégat de flux a un temps écoulé 6,209 secondes supérieur à son enfant immédiat. Et la majeure partie du temps des enfants était occupée par le filtre. Cela correspond aux évaluations d'expressions supplémentaires.


Au fait .... En général, il ce n'est pas une chose sûre que les expressions sous-jacentes avec des étiquettes comme Expr1000 ne sont calculés qu'une seule fois et ne sont pas réévalués, mais clairement dans ce cas, à partir de l'écart de synchronisation d'exécution, cela se produit ici.

15
Martin Smith