Warum verbessert diese abgeleitete Tabelle die Leistung?

18

Ich habe eine Abfrage, die eine JSON-Zeichenfolge als Parameter nimmt. Der json ist ein Array von Breiten- und Längengradpaaren. Eine Beispieleingabe könnte die folgende sein.

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

Es ruft eine TVF auf, die die Anzahl der POIs um einen geografischen Punkt in 1,3,5,10 Meilen Entfernung berechnet.

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

Die Absicht der json-Abfrage besteht darin, diese Funktion als Massenaufruf aufzurufen. Wenn ich es so nenne, ist die Leistung sehr schlecht und dauert fast 10 Sekunden für nur 4 Punkte:

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

Durch das Verschieben der Geografiekonstruktion in eine abgeleitete Tabelle wird die Leistung jedoch erheblich verbessert, und die Abfrage wird in etwa 1 Sekunde abgeschlossen.

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

Die Pläne sehen praktisch identisch aus. Keiner verwendet Parallelität und beide verwenden den räumlichen Index. Es gibt eine zusätzliche träge Spule auf dem langsamen Plan, die ich mit dem Hinweis beseitigen kann option(no_performance_spool). Die Abfrageleistung ändert sich jedoch nicht. Es bleibt immer noch viel langsamer.

Wenn Sie beide mit dem hinzugefügten Hinweis in einem Stapel ausführen, werden beide Abfragen gleich gewichtet.

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

Meine Frage ist also, warum das wichtig ist? Wie kann ich wissen, wann ich Werte in einer abgeleiteten Tabelle berechnen soll oder nicht?

Michael B
quelle
1
Mit "wiegen" meinen Sie geschätzte Kosten in%? Diese Zahl ist praktisch bedeutungslos, insbesondere wenn Sie UDFs,
Aaron Bertrand
Ich bin mir dessen bewusst, aber wenn ich mir die IO-Statistiken anschaue, sind sie auch identisch. Beide führen 358306 logische Lesevorgänge in der point_of_interestTabelle durch, scannen den Index 4602-mal und generieren eine Arbeitstabelle und eine Arbeitsdatei. Der Schätzer ist der Ansicht, dass diese Pläne identisch sind, die Leistung jedoch etwas anderes vorgibt.
Michael B
Anscheinend ist die eigentliche CPU das Problem, wahrscheinlich aufgrund dessen, worauf Martin hingewiesen hat, nicht aufgrund von E / A. Leider basieren die geschätzten Kosten auf der Kombination von CPU und E / A und geben nicht immer wieder, was in der Realität passiert. Wenn Sie mit SentryOne Plan Explorer aktuelle Pläne erstellen ( ich arbeite dort, aber das Tool ist kostenlos und ohne Bedingungen ), ändern Sie die tatsächlichen Kosten in "Nur CPU". Dadurch erhalten Sie möglicherweise bessere Hinweise darauf, wo die gesamte CPU-Zeit ausgegeben wurde.
Aaron Bertrand
1
@MartinSmith Noch nicht pro Betreiber, nein. Wir tauchen auf der Aussageebene auf. Derzeit verlassen wir uns immer noch auf die erste Implementierung von DMV, bevor diese zusätzlichen Metriken auf der unteren Ebene hinzugefügt wurden. Und wir waren ein bisschen damit beschäftigt, an etwas anderem zu arbeiten, das Sie bald sehen werden. :-)
Aaron Bertrand
1
PS Sie können die Leistung noch verbessern, indem Sie eine einfache Rechenbox verwenden, bevor Sie die geradlinige Entfernungsberechnung durchführen. Das heißt, filtern Sie zuerst nach denen, bei denen der Wert |LatLong.Lat - @geo.Lat| + |LatLong.Long - @geo.Long| < numso komplizierter ist, bevor Sie ihn ausführen sqrt((LatLong.Lat - @geo.Lat)^2 + (LatLong.Long - @geo.Long)^2). Und noch besser, berechnen Sie zuerst die obere und untere Grenze LatLong.Lat > @geoLatLowerBound && LatLong.Lat < @geoLatUpperBound && LatLong.Long > @geoLongLowerBound && LatLong.Long < @geoLongUpperBound. (Dies ist ein Pseudocode, passen Sie ihn entsprechend an.)
ErikE

Antworten:

15

Ich kann Ihnen eine teilweise Antwort geben, die erklärt, warum Sie den Leistungsunterschied sehen - obwohl noch einige offene Fragen offen sind (wie kann SQL Server den optimaleren Plan erstellen, ohne einen Zwischentabellenausdruck einzuführen, der den Ausdruck als Spalte projiziert?).


Der Unterschied besteht darin, dass im schnellen Plan die zum Parsen der JSON-Array-Elemente und zum Erstellen der Geografie erforderlichen Arbeiten viermal ausgeführt werden (einmal für jede Zeile, die von der openjsonFunktion ausgegeben wird) - während dies mehr als das 100.000- fache des langsamen Plans ist.

Im schnellen Plan ...

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

Wird Expr1000im Berechnungsskalar links von der openjsonFunktion zugewiesen . Dies entspricht geoin Ihrer abgeleiteten Tabellendefinition.

Bildbeschreibung hier eingeben

Im Schnellplan die Filter- und Stream-Aggregatreferenz Expr1000. Im langsamen Plan verweisen sie auf den vollständigen zugrunde liegenden Ausdruck.

Aggregateigenschaften streamen

Bildbeschreibung hier eingeben

Der Filter wird 116.995 Mal ausgeführt, wobei jede Ausführung eine Ausdrucksbewertung erfordert. In das Stream-Aggregat fließen 110.520 Zeilen zur Aggregation ein, und mithilfe dieses Ausdrucks werden drei separate Aggregate erstellt. 110,520 * 3 + 116,995 = 448,555. Selbst wenn jede einzelne Auswertung 18 Mikrosekunden dauert, ergibt dies eine zusätzliche Zeit von 8 Sekunden für die gesamte Abfrage.

Sie können die Auswirkung davon in der aktuellen Zeitstatistik im Plan-XML sehen (unten in rot vom langsamen Plan und in blau von den schnellen Planzeiten in ms).

Bildbeschreibung hier eingeben

Das Stream-Aggregat hat eine abgelaufene Zeit, die 6,209 Sekunden länger ist als das unmittelbar untergeordnete Element. Und der Großteil der Kinderzeit wurde vom Filter aufgenommen. Dies entspricht den zusätzlichen Ausdrucksauswertungen.


Übrigens ... Im Allgemeinen ist es nicht sicher, dass zugrunde liegende Ausdrücke mit Bezeichnungen wie Expr1000nur einmal berechnet und nicht neu bewertet werden. In diesem Fall ergibt sich dies jedoch eindeutig aus der Abweichung des Ausführungszeitpunkts.

Martin Smith
quelle
Nebenbei, wenn ich die Abfrage so ändere, dass sie zur Erstellung der Geografie ein Kreuz verwendet, erhalte ich auch den Schnellplan. cross apply(select geo=geography::Point( convert(float,json_value(value,'$[0]')) ,convert(float,json_value(value,'$[1]')) ,4326))f
Michael B
Leider, aber ich frage mich, ob es einen einfacheren Weg gibt, den schnellen Plan zu erstellen.
Michael B
Entschuldigen Sie die Amateurfrage, aber welches Tool wird in Ihren Bildern angezeigt?
BlueRaja - Danny Pflughoeft
1
@ BlueRaja-DannyPflughoeft Dies sind Ausführungspläne, die im Management Studio angezeigt werden (die in SSMS verwendeten Symbole wurden in neueren Versionen aktualisiert, wenn dies der Grund für die Frage war)
Martin Smith