Почему эта производная таблица улучшает производительность?

18

У меня есть запрос, который принимает строку JSON в качестве параметра. JSON - это массив пар широты и долготы. Пример ввода может быть следующим.

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

Он вызывает TVF, который вычисляет количество POI вокруг географической точки на расстоянии 1,3,5,10 мили.

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

Целью запроса json является массовый вызов этой функции. Если я назову это так, производительность будет очень плохой и займет около 10 секунд всего за 4 балла:

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

Однако перемещение построения географии внутри производной таблицы приводит к значительному улучшению производительности, завершая запрос примерно за 1 секунду.

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

Планы выглядят практически идентичными. Ни один из них не использует параллелизм, и оба используют пространственный индекс. На медленном плане есть дополнительная ленивая шпуля, которую я могу устранить с помощью подсказки option(no_performance_spool). Но производительность запроса не меняется. Это все еще остается намного медленнее.

Запуск обоих с добавленной подсказкой в ​​пакете будет взвешивать оба запроса одинаково.

Версия Sql-сервера = Microsoft SQL Server 2016 (SP1-CU7-GDR) (KB4057119) - 13.0.4466.4 (X64)

Итак, мой вопрос, почему это имеет значение? Как я могу узнать, когда мне следует вычислять значения внутри производной таблицы или нет?

Майкл Б
источник
1
Под «весом» вы подразумеваете ориентировочную стоимость%? Это число практически бессмысленно, особенно когда вы вводите UDF, JSON, CLR через географию и т. Д.
Аарон Бертран
Я знаю, но, глядя на статистику ввода-вывода, они тоже идентичны. Оба выполняют 358306 логических операций чтения point_of_interestтаблицы, оба сканируют индекс 4602 раза, и оба генерируют рабочий стол и рабочий файл. Оценщик полагает, что эти планы идентичны, но показатели говорят об обратном.
Майкл Б
Кажется, что проблема заключается в реальном процессоре, вероятно, из-за того, что указал Мартин, а не из-за ввода-вывода. К сожалению, оценочные затраты основаны на комбинации процессора и ввода-вывода и не всегда отражают то, что происходит в реальности. Если вы генерируете фактические планы с помощью SentryOne Plan Explorer ( я работаю там, но инструмент бесплатен без каких-либо строк ), а затем меняете фактические затраты только на ЦП, вы можете получить более точные показатели того, на что было потрачено все это ЦП.
Аарон Бертран
1
@MartinSmith Пока не на оператора, нет. Мы делаем это на уровне заявлений. В настоящее время мы все еще полагаемся на первоначальную реализацию из DMV, прежде чем эти дополнительные метрики были добавлены на более низком уровне. И мы были немного заняты работой над чем-то еще, что вы скоро увидите. :-)
Аарон Бертран
1
PS Вы можете добиться еще большего улучшения производительности, выполнив простое арифметическое поле перед выполнением вычисления расстояния по прямой. То есть сначала отфильтруйте те, где значение |LatLong.Lat - @geo.Lat| + |LatLong.Long - @geo.Long| < nперед вами сложнее sqrt((LatLong.Lat - @geo.Lat)^2 + (LatLong.Long - @geo.Long)^2). А еще лучше, сначала рассчитайте верхнюю и нижнюю границы LatLong.Lat > @geoLatLowerBound && LatLong.Lat < @geoLatUpperBound && LatLong.Long > @geoLongLowerBound && LatLong.Long < @geoLongUpperBound. (Это псевдокод, адаптируйтесь соответствующим образом.)
ErikE

Ответы:

15

Я могу дать вам частичный ответ, который объясняет, почему вы видите разницу в производительности - хотя это все еще оставляет некоторые открытые вопросы (например, может ли SQL Server создать более оптимальный план без введения выражения промежуточной таблицы, которое проецирует выражение в виде столбца?)


Разница в том, что в быстром плане работа, необходимая для анализа элементов массива JSON и создания географии, выполняется 4 раза (по одному разу для каждой строки, получаемой из openjsonфункции), тогда как в медленном плане это выполняется более чем в 100 000 раз .

В быстром плане ...

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

Назначается Expr1000в вычисляемом скаляре слева от openjsonфункции. Это соответствует определению geoв вашей производной таблице.

введите описание изображения здесь

В быстром плане фильтр и ссылка на агрегат потока Expr1000. В медленном плане они ссылаются на полное базовое выражение.

Свойства агрегатного потока

введите описание изображения здесь

Фильтр выполняется 116 995 раз, при этом каждое выполнение требует вычисления выражения. Агрегат потока имеет 110 520 строк, входящих в него для агрегации, и создает три отдельных агрегата, используя это выражение. 110,520 * 3 + 116,995 = 448,555, Даже если каждая отдельная оценка занимает 18 микросекунд, это добавляет до 8 секунд дополнительного времени для запроса в целом.

Вы можете увидеть эффект этого в статистике фактического времени в XML-плане (ниже показан медленный план красным и синий для быстрого плана - время указано в мс)

введите описание изображения здесь

Прошедшее время агрегата потока на 6,209 секунды больше, чем его непосредственный дочерний элемент. И большую часть детского времени занял фильтр. Это соответствует дополнительным оценкам выражений.


Между прочим .... В общем, это не совсем то, что базовые выражения с метками наподобие Expr1000рассчитываются только один раз и не переоцениваются, но в данном случае ясно из-за несоответствия времени выполнения, это происходит здесь.

Мартин Смит
источник
Кроме того, если я переключу запрос на перекрестное применение для создания географии, я также получу быстрый план. cross apply(select geo=geography::Point( convert(float,json_value(value,'$[0]')) ,convert(float,json_value(value,'$[1]')) ,4326))f
Майкл Б
К сожалению, но мне интересно, есть ли более простой способ получить быстрый план.
Майкл Б
Извините за любительский вопрос, но какой инструмент показан на ваших изображениях?
BlueRaja - Дэнни Пфлюгофт
1
@ BlueRaja-DannyPflughoeft это планы выполнения, показанные в студии управления (значки, используемые в SSMS, были обновлены в последних версиях, если это было причиной вопроса)
Мартин Смит