Perché questa tabella derivata migliora le prestazioni?


18

Ho una query che accetta una stringa JSON come parametro. Il json è una matrice di coppie di latitudine e longitudine. Un input di esempio potrebbe essere il seguente.

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

Chiama un TVF che calcola il numero di PDI attorno a un punto geografico, a distanze di 1,3,5,10 miglia.

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'intento della query json è chiamare in blocco questa funzione. Se lo chiamo così, le prestazioni sono molto scarse e impiegano quasi 10 secondi per soli 4 punti:

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

Tuttavia, spostando la costruzione della geografia all'interno di una tabella derivata, le prestazioni migliorano notevolmente, completando la query in circa 1 secondo.

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

I piani sembrano praticamente identici. Nessuno dei due usa il parallelismo ed entrambi usano l'indice spaziale. C'è un rocchetto pigro aggiuntivo sul piano lento che posso eliminare con il suggerimento option(no_performance_spool). Ma le prestazioni della query non cambiano. Rimane ancora molto più lento.

L'esecuzione di entrambi con il suggerimento aggiunto in un batch peserà equamente entrambe le query.

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

Quindi la mia domanda è: perché è importante? Come posso sapere quando dovrei calcolare i valori all'interno di una tabella derivata o no?


1
Per "pesare" intendi il costo stimato in%? Quel numero è praticamente insignificante, specialmente quando si inseriscono UDF, JSON, CLR tramite geografia, ecc.
Aaron Bertrand

Sono consapevole, ma guardando le statistiche IO sono identiche. Entrambi eseguono 358306 letture logiche sulla point_of_interesttabella, entrambi scansionano l'indice 4602 volte ed entrambi generano un worktable e un file di lavoro. Lo stimatore ritiene che questi piani siano identici ma le prestazioni dicono diversamente.
Michael B,

Sembra che la vera CPU sia il problema qui, probabilmente a causa di ciò che Martin ha sottolineato, non dell'I / O. Purtroppo i costi stimati si basano su CPU e I / O combinati e non riflettono sempre ciò che accade nella realtà. Se generi piani effettivi utilizzando SentryOne Plan Explorer ( ci lavoro, ma lo strumento è gratuito senza stringhe ), quindi modifica i costi effettivi solo per la CPU, potresti ottenere migliori indicatori di dove è stato impiegato tutto quel tempo della CPU.
Aaron Bertrand

1
@MartinSmith Non per operatore ancora, no. Facciamo emergere quelli a livello di istruzione. Attualmente facciamo ancora affidamento sull'implementazione iniziale del DMV prima che tali metriche aggiuntive siano state aggiunte al livello inferiore. E siamo stati un po 'impegnati a lavorare su qualcos'altro che vedrai presto. :-)
Aaron Bertrand

1
PS È possibile ottenere un miglioramento ancora maggiore delle prestazioni eseguendo una semplice casella aritmetica prima di eseguire il calcolo della distanza in linea retta. Cioè, prima filtrare per quelli in cui il valore |LatLong.Lat - @geo.Lat| + |LatLong.Long - @geo.Long| < nprima di fare il più complicato sqrt((LatLong.Lat - @geo.Lat)^2 + (LatLong.Long - @geo.Long)^2). E ancora meglio, calcolare prima i limiti superiore e inferiore, quindi LatLong.Lat > @geoLatLowerBound && LatLong.Lat < @geoLatUpperBound && LatLong.Long > @geoLongLowerBound && LatLong.Long < @geoLongUpperBound. (Questo è pseudocodice, adattarsi in modo appropriato.)
ErikE

Risposte:


15

Posso darti una risposta parziale che spiega perché stai vedendo la differenza di prestazioni - anche se ciò lascia ancora alcune domande aperte (come SQL Server può produrre il piano più ottimale senza introdurre un'espressione di tabella intermedia che proietta l'espressione come una colonna?)


La differenza è che nel piano veloce il lavoro necessario per analizzare gli elementi dell'array JSON e creare la geografia viene eseguito 4 volte (una volta per ogni riga emessa dalla openjsonfunzione) - mentre viene eseguito più di 100.000 volte rispetto al piano lento.

Nel piano veloce ...

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

È assegnato a Expr1000nello scalare di calcolo a sinistra della openjsonfunzione. Ciò corrisponde a geonella definizione della tabella derivata.

inserisci qui la descrizione dell'immagine

Nel piano veloce il filtro e il flusso aggregano il riferimento Expr1000. Nel piano lento fanno riferimento all'espressione sottostante completa.

Streaming proprietà aggregate

inserisci qui la descrizione dell'immagine

Il filtro viene eseguito 116.995 volte con ogni esecuzione che richiede una valutazione dell'espressione. L'aggregato del flusso ha 110.520 righe che fluiscono al suo interno per l'aggregazione e crea tre aggregati separati usando questa espressione. 110,520 * 3 + 116,995 = 448,555. Anche se ogni singola valutazione richiede 18 microsecondi, questo aggiunge fino a 8 secondi di tempo in più per l'intera query.

Puoi vedere l'effetto di questo nelle statistiche temporali effettive nel piano XML (annotato in rosso sotto dal piano lento e blu per il piano veloce - i tempi sono in ms)

inserisci qui la descrizione dell'immagine

L'aggregato del flusso ha un tempo trascorso di 6,209 secondi maggiore del suo figlio immediato. E la maggior parte del tempo del bambino è stata occupata dal filtro. Ciò corrisponde alle valutazioni delle espressioni extra.


A proposito .... In generale non è una cosa certa che le espressioni sottostanti con etichette come Expr1000siano calcolate una sola volta e non rivalutate, ma chiaramente in questo caso dalla discrepanza nei tempi di esecuzione ciò accade qui.


Per inciso, se cambio la query per utilizzare una croce per generare la geografia, ottengo anche il piano veloce. cross apply(select geo=geography::Point( convert(float,json_value(value,'$[0]')) ,convert(float,json_value(value,'$[1]')) ,4326))f
Michael B,

Sfortunato, ma mi chiedo se c'è un modo più semplice per farlo generare il piano veloce.
Michael B,

Ci scusiamo per la domanda amatoriale, ma quale strumento è mostrato nelle tue immagini?
BlueRaja - Danny Pflughoeft

1
@ BlueRaja-DannyPflughoeft questi sono piani di esecuzione mostrati in Management Studio (le icone utilizzate in SSMS sono state aggiornate nelle versioni recenti se questo era il motivo della domanda)
Martin Smith
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.