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

18

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 paresseuse 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 les deux requêtes de manière égale.

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?

Michael B
la source
1
Par «peser», voulez-vous dire le coût estimé%? Ce nombre n'a pratiquement aucun sens, surtout lorsque vous importez des UDF, JSON, CLR via la géographie, etc.
Aaron Bertrand
Je suis conscient, mais en regardant les statistiques d'E / S, elles sont également identiques. Les deux effectuent des lectures logiques 358306 sur la point_of_interesttable, analysent l'index 4602 fois et génèrent une table de travail et un fichier de travail. L'estimateur estime que ces plans sont identiques, mais le rendement dit le contraire.
Michael B
Il semble que le CPU réel soit le problème ici, probablement en raison de ce que Martin a souligné, pas des E / S. Malheureusement, les coûts estimés sont basés sur le processeur et les E / S combinés et ne reflètent pas toujours ce qui se passe dans la réalité. Si vous générez des plans réels à l'aide de SentryOne Plan Explorer ( j'y travaille, mais l'outil est gratuit sans chaînes ), puis changez les coûts réels en CPU uniquement, vous obtiendrez peut-être de meilleurs indicateurs de l'endroit où tout ce temps CPU a été dépensé.
Aaron Bertrand
1
@MartinSmith Pas encore par opérateur, non. Nous les faisons ressortir au niveau des déclarations. Actuellement, nous nous appuyons toujours sur l'implémentation initiale du DMV avant que ces mesures supplémentaires soient ajoutées au niveau inférieur. Et nous avons été un peu occupés à travailler sur autre chose que vous verrez bientôt. :-)
Aaron Bertrand
1
PS Vous pouvez obtenir encore plus d'amélioration des performances en faisant une simple boîte arithmétique avant de faire le calcul de la distance en ligne droite. Autrement dit, filtrez d'abord ceux dont la valeur |LatLong.Lat - @geo.Lat| + |LatLong.Long - @geo.Long| < navant de faire le plus compliqué sqrt((LatLong.Lat - @geo.Lat)^2 + (LatLong.Long - @geo.Long)^2). Et encore mieux, calculez d'abord les limites supérieures et inférieures LatLong.Lat > @geoLatLowerBound && LatLong.Lat < @geoLatUpperBound && LatLong.Long > @geoLongLowerBound && LatLong.Long < @geoLongUpperBound. (Ceci est un pseudocode, adaptez
conséquence

Réponses:

15

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 (comme SQL Server peut-il produire le plan le 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 openjsonfonction) - alors qu'il est effectué plus de 100000 fois par rapport au plan lent.

Dans le plan rapide ...

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

Est affecté à Expr1000dans le scalaire de calcul à gauche de la openjsonfonction. Cela correspond à geovotre définition de table dérivée.

entrez la description de l'image ici

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

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

entrez la description de l'image ici

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éel dans le plan XML (annoté en rouge ci-dessous du plan lent et en bleu pour le plan rapide - les temps sont en ms)

entrez la description de l'image ici

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.


Soit dit en passant ... En général, il n'est pas sûr que les expressions sous-jacentes avec des étiquettes comme Expr1000ne soient calculées qu'une seule fois et ne soient pas réévaluées, mais clairement dans ce cas à partir de la différence de synchronisation d'exécution, cela se produit ici.

Martin Smith
la source
En passant, si je change la requête pour utiliser une application croisée pour générer la géographie, j'obtiens également le plan rapide. cross apply(select geo=geography::Point( convert(float,json_value(value,'$[0]')) ,convert(float,json_value(value,'$[1]')) ,4326))f
Michael B
Malheureusement, mais je me demande s'il existe un moyen plus simple de générer le plan rapide.
Michael B
Désolé pour la question amateur, mais quel outil apparaît dans vos images?
BlueRaja - Danny Pflughoeft
1
@ BlueRaja-DannyPflughoeft ce sont des plans d'exécution affichés dans le studio de gestion (les icônes utilisées dans SSMS ont été mises à jour dans les versions récentes si c'était la raison de la question)
Martin Smith