为什么此派生表可以提高性能?


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,该TVF可以计算在1、3、5、10英里距离处某个地理位置周围的POI数量。

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查询的目的是批量调用此函数。如果我这样称呼,性能非常差,仅用4分就花费了将近10秒:

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))

计划= 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)

计划= https://www.brentozar.com/pastetheplan/?id=HkSS5_OoE

这些计划看起来几乎是相同的。都不使用并行性,也不使用空间索引。在缓慢的计划中还有一个额外的懒惰假脱机,可以通过提示来消除option(no_performance_spool)。但是查询性能不会改变。它仍然慢得多。

批量运行带有添加提示的两个查询,将相等地权衡两个查询。

SQL Server版本= Microsoft SQL Server 2016(SP1-CU7-GDR)(KB4057119)-13.0.4466.4(X64)

所以我的问题是为什么这很重要?我怎么知道何时应该在派生表中计算值?


1
“称量”是指估计成本%?这个数字几乎是毫无意义的,特别是当你带来的UDF,JSON,通过地理等CLR
阿龙贝特朗

我知道,但是查看IO统计信息,它们也相同。两者均在point_of_interest表上进行358306逻辑读取,均扫描索引4602次,并均生成工作表和工作文件。估计者认为这些计划是相同的,但性能则相反。
Michael B

看来实际的CPU是这里的问题,可能是由于Martin指出的,而不是I / O。不幸的是,估计成本是基于CPU和I / O的总和,并不总能反映实际情况。如果您使用SentryOne Plan Explorer生成实际计划(我在那儿工作,但是该工具是免费的,没有任何字符串),然后将实际成本更改为仅CPU,则可能会更好地指示所有CPU时间所花费的位置。
亚伦·伯特兰

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

Answers:


15

我可以给你解释为什么你看到的性能差异了部分答案-尽管这仍然留下了一些开放性的问题(如CAN SQL服务器产生更优的方案,而不会引入一个中间表表达式项目表达为列?)


不同之处在于,在快速计划中,解析JSON数组元素和创建Geography所需的工作完成了4次(对于从openjson函数发出的每一行一次),而在慢速计划中则完成了100,000 倍以上

在快速计划中...

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

Expr1000openjson函数左侧的计算标量中分配给。这对应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
Michael B

不幸的是,但是我想知道是否有更简单的方法来生成快速计划。
Michael B

对不起,业余问题,但是您的图像中显示了什么工具?
BlueRaja-Danny Pflughoeft

1
@ BlueRaja-DannyPflughoeft这些是管理工作室中显示的执行计划(如果这是问题的原因,SSMS中使用的图标已更新为最新版本)
马丁·史密斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.