¿Por qué esta tabla derivada mejora el rendimiento?

18

Tengo una consulta que toma una cadena json como parámetro. El json es un conjunto de pares de latitud y longitud. Un ejemplo de entrada podría ser el siguiente.

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

Llama a un TVF que calcula la cantidad de PDI alrededor de un punto geográfico, a distancias de 1,3,5,10 millas.

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

La intención de la consulta json es llamar en masa a esta función. Si lo llamo así, el rendimiento es muy pobre, tomando casi 10 segundos por solo 4 puntos:

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

Sin embargo, mover la construcción de la geografía dentro de una tabla derivada hace que el rendimiento mejore dramáticamente, completando la consulta en aproximadamente 1 segundo.

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

Los planes se ven prácticamente idénticos. Ninguno usa paralelismo y ambos usan el índice espacial. Hay un carrete perezoso adicional en el plan lento que puedo eliminar con la pista option(no_performance_spool). Pero el rendimiento de la consulta no cambia. Sigue siendo mucho más lento.

Ejecutar ambos con la sugerencia agregada en un lote pesará ambas consultas por igual.

Versión del servidor SQL = Microsoft SQL Server 2016 (SP1-CU7-GDR) (KB4057119) - 13.0.4466.4 (X64)

Entonces mi pregunta es ¿por qué esto importa? ¿Cómo puedo saber cuándo debo calcular valores dentro de una tabla derivada o no?

Michael B
fuente
1
¿Por "pesar" quiere decir% de costo estimado? Ese número prácticamente no tiene sentido, especialmente cuando traes UDF, JSON, CLR a través de la geografía, etc.
Aaron Bertrand
Soy consciente, pero mirando las estadísticas de IO también son idénticas. Ambos realizan 358306 lecturas lógicas en la point_of_interesttabla, ambos escanean el índice 4602 veces y ambos generan una tabla de trabajo y un archivo de trabajo. El estimador cree que estos planes son idénticos pero el rendimiento dice lo contrario.
Michael B
Parece que la CPU real es el problema aquí, probablemente debido a lo que Martin señaló, no a E / S. Desafortunadamente, los costos estimados se basan en CPU y E / S combinadas y no siempre reflejan lo que sucede en la realidad. Si genera planes reales utilizando SentryOne Plan Explorer ( trabajo allí, pero la herramienta es gratuita y sin cadenas ), luego cambie los costos reales a la CPU solamente, podría obtener mejores indicadores de dónde se gastó todo el tiempo de la CPU.
Aaron Bertrand
1
@MartinSmith Aún no por operador, no. Hacemos emerger aquellos a nivel de declaración. Actualmente todavía confiamos en la implementación inicial del DMV antes de que se agreguen esas métricas adicionales en el nivel inferior. Y hemos estado un poco ocupados trabajando en otra cosa que verás pronto. :-)
Aaron Bertrand
1
PD Puede obtener una mejora aún mayor en el rendimiento al hacer un cuadro aritmético simple antes de hacer el cálculo de la distancia en línea recta. Es decir, filtre primero para aquellos donde el valor |LatLong.Lat - @geo.Lat| + |LatLong.Long - @geo.Long| < nantes de hacerlo sea más complicado sqrt((LatLong.Lat - @geo.Lat)^2 + (LatLong.Long - @geo.Long)^2). Y aún mejor, primero calcule los límites superior e inferior LatLong.Lat > @geoLatLowerBound && LatLong.Lat < @geoLatUpperBound && LatLong.Long > @geoLongLowerBound && LatLong.Long < @geoLongUpperBound. (Esto es pseudocódigo, adaptarse adecuadamente.)
ErikE

Respuestas:

15

Puedo darle una respuesta parcial que explique por qué está viendo la diferencia de rendimiento, aunque eso todavía deja algunas preguntas abiertas (como ¿ puede SQL Server producir el plan más óptimo sin introducir una expresión de tabla intermedia que proyecte la expresión como una columna?)


La diferencia es que en el plan rápido, el trabajo necesario para analizar los elementos de la matriz JSON y crear la Geografía se realiza 4 veces (una por cada fila emitida por la openjsonfunción), mientras que se realiza más de 100,000 veces que en el plan lento.

En el plan rápido ...

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

Se asigna a Expr1000en el escalar de cálculo a la izquierda de la openjsonfunción. Esto corresponde a geosu definición de tabla derivada.

ingrese la descripción de la imagen aquí

En el plan rápido, el filtro y la secuencia agregan referencia Expr1000. En el plan lento hacen referencia a la expresión subyacente completa.

Propiedades agregadas de flujo

ingrese la descripción de la imagen aquí

El filtro se ejecuta 116,995 veces y cada ejecución requiere una evaluación de expresión. El agregado de flujo tiene 110,520 filas que fluyen hacia él para la agregación y crea tres agregados separados usando esta expresión. 110,520 * 3 + 116,995 = 448,555. Incluso si cada evaluación individual toma 18 microsegundos, esto agrega hasta 8 segundos de tiempo adicional para la consulta en su conjunto.

Puede ver el efecto de esto en las estadísticas de tiempo real en el plan XML (anotado en rojo debajo del plan lento y azul para el plan rápido; los tiempos están en ms)

ingrese la descripción de la imagen aquí

El agregado de flujo tiene un tiempo transcurrido 6.209 segundos mayor que su hijo inmediato. Y el filtro ocupó la mayor parte del tiempo del niño. Esto corresponde a las evaluaciones de expresión extra.


Por cierto ... En general, no es seguro que las expresiones subyacentes con etiquetas como Expr1000solo se calculen una vez y no se vuelvan a evaluar, pero claramente en este caso a partir de la discrepancia en el tiempo de ejecución que sucede aquí.

Martin Smith
fuente
Por otro lado, si cambio la consulta para usar una aplicación cruzada para generar la geografía, también obtengo el plan rápido. cross apply(select geo=geography::Point( convert(float,json_value(value,'$[0]')) ,convert(float,json_value(value,'$[1]')) ,4326))f
Michael B
Desafortunadamente, pero me pregunto si hay una manera más fácil de lograr que genere el plan rápido.
Michael B
Perdón por la pregunta amateur, pero ¿qué herramienta se muestra en tus imágenes?
BlueRaja - Danny Pflughoeft
1
@ BlueRaja-DannyPflughoeft estos son planes de ejecución que se muestran en el estudio de administración (los iconos utilizados en SSMS se han actualizado en versiones recientes si ese fue el motivo de la pregunta)
Martin Smith