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

Mic*_*l B 18 sql-server sql-server-2016

我有一个以 json 字符串作为参数的查询。json 是一个纬度、经度对数组。示例输入可能如下所示。

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

它调用 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
Run Code Online (Sandbox Code Playgroud)

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))
Run Code Online (Sandbox Code Playgroud)

计划 = 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)
Run Code Online (Sandbox Code Playgroud)

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

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

Mar*_*ith 15

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


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

在快速计划...

geography::Point(
                convert(float,json_value(value,'$[0]'))
               ,convert(float,json_value(value,'$[1]'))
               ,4326)
Run Code Online (Sandbox Code Playgroud)

Expr1000openjson函数左侧的计算标量中分配给。这对应geo于您的派生表定义。

在此处输入图片说明

在快速计划中过滤器和流聚合参考Expr1000。在慢速计划中,他们引用了完整的底层表达式。

流聚合属性

在此处输入图片说明

过滤器执行 116,995 次,每次执行都需要表达式计算。流聚合有 110,520 行流入其中进行聚合,并使用此表达式创建三个单独的聚合。110,520 * 3 + 116,995 = 448,555. 即使每个单独的评估需要 18 微秒,这也会为整个查询增加 8 秒的额外时间。

您可以在计划 XML 中的实际时间统计信息中看到此效果(下面用红色标注慢速计划,蓝色标注快速计划 - 时间以毫秒为单位)

在此处输入图片说明

流聚合的经过时间比其直接子级大 6.209 秒。大部分孩子的时间都被过滤器占用了。这对应于额外的表达式评估。


顺便说一句......一般来说,带有标签的底层表达式只计算一次而不是重新评估是不确定的Expr1000但在这种情况下,由于执行时间差异,这很明显。