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?
la source
point_of_interest
table, 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.|LatLong.Lat - @geo.Lat| + |LatLong.Long - @geo.Long| < n
avant 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érieuresLatLong.Lat > @geoLatLowerBound && LatLong.Lat < @geoLatUpperBound && LatLong.Long > @geoLongLowerBound && LatLong.Long < @geoLongUpperBound
. (Ceci est un pseudocode, adaptezRéponses:
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
openjson
fonction) - alors qu'il est effectué plus de 100000 fois par rapport au plan lent.Dans le plan rapide ...
Est affecté à
Expr1000
dans le scalaire de calcul à gauche de laopenjson
fonction. Cela correspond àgeo
votre définition de table dérivée.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
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)
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
Expr1000
ne 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.la source
cross apply(select geo=geography::Point( convert(float,json_value(value,'$[0]')) ,convert(float,json_value(value,'$[1]')) ,4326))f