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?
fuente
point_of_interest
tabla, 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.|LatLong.Lat - @geo.Lat| + |LatLong.Long - @geo.Long| < n
antes de hacerlo sea más complicadosqrt((LatLong.Lat - @geo.Lat)^2 + (LatLong.Long - @geo.Long)^2)
. Y aún mejor, primero calcule los límites superior e inferiorLatLong.Lat > @geoLatLowerBound && LatLong.Lat < @geoLatUpperBound && LatLong.Long > @geoLongLowerBound && LatLong.Long < @geoLongUpperBound
. (Esto es pseudocódigo, adaptarse adecuadamente.)Respuestas:
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
openjson
función), mientras que se realiza más de 100,000 veces que en el plan lento.En el plan rápido ...
Se asigna a
Expr1000
en el escalar de cálculo a la izquierda de laopenjson
función. Esto corresponde ageo
su definición de tabla derivada.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
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)
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
Expr1000
solo 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í.fuente
cross apply(select geo=geography::Point( convert(float,json_value(value,'$[0]')) ,convert(float,json_value(value,'$[1]')) ,4326))f