Dlaczego ta tabela pochodna poprawia wydajność?

18

Mam zapytanie, które bierze ciąg json jako parametr. Json to tablica par szerokości i długości geograficznej. Przykładowe dane wejściowe mogą być następujące.

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

Wzywa TVF, który oblicza liczbę punktów POI w pobliżu punktu geograficznego, w odległości 1,3,5,10 mil.

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

Celem zapytania json jest zbiorcze wywołanie tej funkcji. Jeśli nazywam to w ten sposób, wydajność jest bardzo niska i zajmuje prawie 10 sekund za zaledwie 4 punkty:

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

Jednak przesunięcie konstrukcji geograficznej wewnątrz tabeli pochodnej powoduje znaczną poprawę wydajności, wypełniając zapytanie w około 1 sekundę.

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

Plany wyglądają praktycznie identycznie. Żadne z nich nie używa równoległości i oba wykorzystują indeks przestrzenny. Na wolnym planie jest dodatkowa leniwa szpula, którą mogę wyeliminować za pomocą podpowiedzi option(no_performance_spool). Ale wydajność zapytania nie zmienia się. Nadal pozostaje znacznie wolniejszy.

Uruchomienie obu z dodaną wskazówką w partii spowoduje równe ważenie obu zapytań.

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

Więc moje pytanie brzmi: dlaczego to ma znaczenie? Skąd mam wiedzieć, kiedy powinienem obliczyć wartości w tabeli pochodnej, czy nie?

Michael B.
źródło
1
Przez „ważenie” rozumiesz procentowy koszt szacunkowy? Ta liczba jest praktycznie bez znaczenia, zwłaszcza gdy wprowadzasz UDF, JSON, CLR przez geografię itp.
Aaron Bertrand
Wiem, ale patrząc na statystyki IO są one również identyczne. Oba wykonują 358306 logicznych odczytów w point_of_interesttabeli, oba skanują indeks 4602 razy i oba generują stół roboczy i plik roboczy. Estymator uważa, że ​​te plany są identyczne, ale wyniki mówią inaczej.
Michael B
Wygląda na to, że problem dotyczy właśnie procesora, prawdopodobnie ze względu na to, co zauważył Martin, a nie we / wy. Niestety szacunkowe koszty są oparte na połączeniu procesora i we / wy i nie zawsze odzwierciedlają to, co dzieje się w rzeczywistości. Jeśli generujesz rzeczywiste plany za pomocą SentryOne Plan Explorer ( pracuję tam, ale narzędzie jest bezpłatne bez żadnych ciągów ), a następnie zmień rzeczywiste koszty tylko na procesor, możesz uzyskać lepsze wskaźniki tego, gdzie cały ten czas procesora został spędzony.
Aaron Bertrand
1
@MartinSmith Jeszcze nie na operatora, nie. Prezentujemy je na poziomie instrukcji. Obecnie nadal polegamy na początkowej implementacji DMV, zanim te dodatkowe wskaźniki zostały dodane na niższym poziomie. Pracowaliśmy nad czymś, co wkrótce zobaczysz. :-)
Aaron Bertrand
1
PS Możesz uzyskać jeszcze większą poprawę wydajności, wykonując proste pole arytmetyczne przed obliczeniem odległości w linii prostej. Oznacza to, że najpierw filtruj dla tych, których wartość |LatLong.Lat - @geo.Lat| + |LatLong.Long - @geo.Long| < nprzed wykonaniem jest bardziej skomplikowana sqrt((LatLong.Lat - @geo.Lat)^2 + (LatLong.Long - @geo.Long)^2). A jeszcze lepiej, najpierw oblicz górną i dolną granicę LatLong.Lat > @geoLatLowerBound && LatLong.Lat < @geoLatUpperBound && LatLong.Long > @geoLongLowerBound && LatLong.Long < @geoLongUpperBound. (To pseudokod, odpowiednio się dostosuj).
ErikE

Odpowiedzi:

15

Mogę dać ci częściową odpowiedź, która wyjaśnia, dlaczego widzisz różnicę w wydajności - choć wciąż pozostawia to kilka otwartych pytań (na przykład czy SQL Server może stworzyć bardziej optymalny plan bez wprowadzania wyrażenia tabeli pośredniej, która wyświetla to wyrażenie jako kolumnę?)


Różnica polega na tym, że w szybkim planie praca potrzebna do parsowania elementów tablicy JSON i utworzenia geografii jest wykonywana 4 razy (raz dla każdego wiersza emitowanego z openjsonfunkcji) - podczas gdy jest wykonywana ponad 100 000 razy w porównaniu z wolnym planem.

W szybkim planie ...

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

Jest przypisany do Expr1000skalaru obliczeniowego po lewej stronie openjsonfunkcji. To odpowiada geodefinicji tabeli pochodnej.

wprowadź opis zdjęcia tutaj

W szybkim planie odwołanie do filtru i strumienia agreguje odniesienie Expr1000. W wolnym planie odnoszą się do pełnego wyrażenia leżącego u podstaw.

Strumień właściwości agregujących

wprowadź opis zdjęcia tutaj

Filtr jest wykonywany 116 995 razy, a każde wykonanie wymaga oceny wyrażenia. Agregacja strumienia ma 110.520 wierszy wpływających do niego w celu agregacji i tworzy trzy oddzielne agregaty za pomocą tego wyrażenia. 110,520 * 3 + 116,995 = 448,555. Nawet jeśli każda indywidualna ocena zajmuje 18 mikrosekund, daje to do 8 sekund dodatkowego czasu na zapytanie jako całość.

Możesz zobaczyć efekt tego w statystykach czasu rzeczywistego w XML planu (adnotacje na czerwono poniżej z wolnego planu i niebieskie dla szybkiego planu - czasy w ms)

wprowadź opis zdjęcia tutaj

Agregacja strumienia ma czas, który upłynął o 6,209 sekundy dłuższy niż jego bezpośrednie dziecko. I większość czasu dziecięcego została zajęta przez filtr. Odpowiada to dodatkowym ocenom wyrażeń.


Nawiasem mówiąc ... Ogólnie rzecz biorąc, nie jest pewne, czy wyrażenia leżące u podstaw takich etykiet Expr1000są obliczane tylko raz i nie są ponownie oceniane, ale w tym przypadku wyraźnie z rozbieżności czasu wykonania, co się tutaj dzieje.

Martin Smith
źródło
Nawiasem mówiąc, jeśli zmienię zapytanie i zastosuję krzyżyk do wygenerowania geografii, otrzymam również szybki plan. cross apply(select geo=geography::Point( convert(float,json_value(value,'$[0]')) ,convert(float,json_value(value,'$[1]')) ,4326))f
Michael B
Niefortunnie, ale zastanawiam się, czy istnieje łatwiejszy sposób, aby uzyskać szybki plan.
Michael B
Przepraszam za amatorskie pytanie, ale jakie narzędzie jest pokazane na twoich obrazach?
BlueRaja - Danny Pflughoeft
1
@ BlueRaja-DannyPflughoeft są to plany wykonania pokazane w studio zarządzania (ikony używane w SSMS zostały zaktualizowane w najnowszych wersjach, jeśli to było przyczyną pytania)
Martin Smith