【问题标题】:Multi step table value function faster than inline table value function多步表值函数比内联表值函数更快
【发布时间】:2022-01-07 14:25:05
【问题描述】:

我知道,我知道。不应该是这样的。

大局,我正在处理地图数据并试图确定公交车站离哪条街道最近。一条街道由一系列点组成。有些街道有 2 个点,但大多数街道大约有 1/2 个。

我的积分表如下所示:

CREATE TABLE [dbo].[MapStreetsPoints](
    [FeatureId] [int] NOT NULL,
    [PointNumber] [int] NOT NULL,
    [Latitude] [decimal](8, 5) NOT NULL,
    [Longitude] [decimal](8, 5) NOT NULL,
 CONSTRAINT [PK_MapStreetsPoints] PRIMARY KEY CLUSTERED 
(
    [FeatureId] ASC,
    [PointNumber] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY]
) ON [PRIMARY]

来自 MapStreetsPoints 的样本数据

FeatureId   PointNumber Latitude   Longitude
----------- ----------- ---------- ----------
1           1           39.81396   -75.83017
1           2           39.81392   -75.83019
1           3           39.81387   -75.83018
1           4           39.81344   -75.83003
1           5           39.81339   -75.83000
1           6           39.81336   -75.82996
1           7           39.81333   -75.82990
1           8           39.81332   -75.82983
1           9           39.81332   -75.82977
1           10          39.81335   -75.82972
2           1           39.72170   -76.11486
2           2           39.72209   -76.11482
2           3           39.72248   -76.11474
2           4           39.72279   -76.11457
2           5           39.72362   -76.11404
2           6           39.72418   -76.11364
2           7           39.72482   -76.11321
2           8           39.72526   -76.11296
2           9           39.72560   -76.11282
2           10          39.72597   -76.11275
2           11          39.72644   -76.11274

这个表大约有 200 万行。

基本上,我将行过滤为可管理的内容,将表自我连接到自身,应用缓慢的顺序并占据前 1 行。我这样做是为了识别最接近某个点的特征。

我的内联表值函数如下所示。

ALTER FUNCTION dbo.fnMapReverseGeocodeNearestStreet
(   
    @Longitude Decimal(8,5),
    @Latitude Decimal(8,5)
)
RETURNS TABLE 
AS
RETURN 
(
    Select  Top 1 
            A.FeatureId
    From    MapStreetsPoints As A
            Inner Join MapStreetsPoints As B
                On A.FeatureId = B.FeatureId
                And A.PointNumber = B.PointNumber - 1
    Where   A.Longitude Between     Convert(Decimal(8,5), @Longitude - 0.002) 
                                    and Convert(Decimal(8,5), @Longitude + 0.002)
            And A.Latitude Between  Convert(Decimal(8,5), @Latitude - 0.002)
                                    And Convert(Decimal(8,5), @Latitude + 0.002)
            And B.Longitude Between Convert(Decimal(8,5), @Longitude - 0.002) 
                                    and Convert(Decimal(8,5), @Longitude + 0.002)
            And B.Latitude Between  Convert(Decimal(8,5), @Latitude - 0.002)
                                    And Convert(Decimal(8,5), @Latitude + 0.002)
    Order By dbo.PerpendicularDistanceToLine(@Longitude, @Latitude, 
                A.Longitude, 
                A.Latitude, 
                B.Longitude, 
                B.Latitude)
)

多步表值函数如下所示。

Alter FUNCTION dbo.fnMapReverseGeocodeNearestStreet_2
(   
    @Longitude Decimal(8,5),
    @Latitude Decimal(8,5)
)
RETURNS @Output TABLE (FeatureId Int)
AS
BEGIN
    -- Fill the table variable with the rows for your result set
    Declare @Temp 
    Table   (
                FeatureId Int,
                StartLongitude Decimal(8,5),
                StartLatitude Decimal(8,5),
                EndLongitude Decimal(8,5),
                EndLatitude Decimal(8,5)
            );
        
    Insert 
    Into    @Temp(FeatureId, StartLongitude, StartLatitude, EndLongitude, EndLatitude)
    Select  A.FeatureId,
            A.Longitude As StartLongitude,
            A.Latitude As StartLatitude,
            B.Longitude As EndLongitude,
            B.Latitude As EndLatitude
    From    MapStreetsPoints As A
            Inner Join MapStreetsPoints As B
                On A.FeatureId = B.FeatureId
                And A.PointNumber = B.PointNumber - 1
    Where   A.Longitude Between     Convert(Decimal(8,5), @Longitude - 0.002) 
                                    and Convert(Decimal(8,5), @Longitude + 0.002)
            And A.Latitude Between  Convert(Decimal(8,5), @Latitude - 0.002)
                                    And Convert(Decimal(8,5), @Latitude + 0.002)
            And B.Longitude Between Convert(Decimal(8,5), @Longitude - 0.002) 
                                    and Convert(Decimal(8,5), @Longitude + 0.002)
            And B.Latitude Between  Convert(Decimal(8,5), @Latitude - 0.002)
                                    And Convert(Decimal(8,5), @Latitude + 0.002);

        Insert
        Into    @Output(FeatureId)
        Select  Top 1 FeatureId
        From    @Temp T
        Order By dbo.PerpendicularDistanceToLine(@Longitude, @Latitude, T.StartLongitude, T.StartLatitude, T.EndLongitude, T.EndLatitude);


    RETURN 
END

逻辑是一样的。不同之处在于我在应用慢速顺序之前加载了一个带有中间结果的表变量。在我看来,排序是在过滤之前应用的。

为了测试这一点,我使用了以下查询:

Select  Map.FeatureId,
        BusStop.BusStopId1,
        BusStop.Description,
        MapStreets.Hazardous
From    BusStop
        Cross Apply dbo.fnMapReverseGeocodeNearestStreet(BusStop.XCoord,BusStop.YCoord) As Map
        Inner Join MapStreets
            On Map.FeatureId = MapStreets.FeatureId

BusStop 表中有大约 2,000 行。

当我运行内联表值函数的测试代码时,需要 22 秒。使用多步表值功能,耗时17秒。

XML Version of showplan

itvf的演出计划:

  |--Nested Loops(Inner Join, OUTER REFERENCES:([A].[FeatureId], [Expr1008]) WITH UNORDERED PREFETCH)
       |--Nested Loops(Inner Join, OUTER REFERENCES:([**DatabaseName**].[dbo].[BusStop].[XCoord], [**DatabaseName**].[dbo].[BusStop].[YCoord]))
       |    |--Index Scan(OBJECT:([**DatabaseName**].[dbo].[BusStop].[idx_BusStop_Description]))
       |    |--Index Spool(SEEK:([**DatabaseName**].[dbo].[BusStop].[XCoord]=[**DatabaseName**].[dbo].[BusStop].[XCoord] AND [**DatabaseName**].[dbo].[BusStop].[YCoord]=[**DatabaseName**].[dbo].[BusStop].[YCoord]))
       |         |--Sort(TOP 1, ORDER BY:([Expr1004] ASC))
       |              |--Compute Scalar(DEFINE:([Expr1004]=[**DatabaseName**].[dbo].[PerpendicularDistanceToLine]([**DatabaseName**].[dbo].[BusStop].[XCoord],[**DatabaseName**].[dbo].[BusStop].[YCoord],[**DatabaseName**].[dbo].[MapStreetsPoints].[Longitude] as [A].[Longitude],[**DatabaseName**].[dbo].[MapStreetsPoints].[Latitude] as [A].[Latitude],[**DatabaseName**].[dbo].[MapStreetsPoints].[Longitude] as [B].[Longitude],[**DatabaseName**].[dbo].[MapStreetsPoints].[Latitude] as [B].[Latitude])))
       |                   |--Hash Match(Inner Join, HASH:([A].[FeatureId], [A].[PointNumber])=([B].[FeatureId], [Expr1007]), RESIDUAL:([**DatabaseName**].[dbo].[MapStreetsPoints].[FeatureId] as [A].[FeatureId]=[**DatabaseName**].[dbo].[MapStreetsPoints].[FeatureId] as [B].[FeatureId] AND [**DatabaseName**].[dbo].[MapStreetsPoints].[PointNumber] as [A].[PointNumber]=[Expr1007]))
       |                        |--Index Seek(OBJECT:([**DatabaseName**].[dbo].[MapStreetsPoints].[MapStreetsPoints4] AS [A]), SEEK:([A].[Longitude] >= CONVERT(decimal(8,5),[**DatabaseName**].[dbo].[BusStop].[XCoord]-(0.002),0) AND [A].[Longitude] <= CONVERT(decimal(8,5),[**DatabaseName**].[dbo].[BusStop].[XCoord]+(0.002),0)),  WHERE:([**DatabaseName**].[dbo].[MapStreetsPoints].[Latitude] as [A].[Latitude]>=CONVERT(decimal(8,5),[**DatabaseName**].[dbo].[BusStop].[YCoord]-(0.002),0) AND [**DatabaseName**].[dbo].[MapStreetsPoints].[Latitude] as [A].[Latitude]<=CONVERT(decimal(8,5),[**DatabaseName**].[dbo].[BusStop].[YCoord]+(0.002),0)) ORDERED FORWARD)
       |                        |--Compute Scalar(DEFINE:([Expr1007]=[**DatabaseName**].[dbo].[MapStreetsPoints].[PointNumber] as [B].[PointNumber]-(1)))
       |                             |--Index Seek(OBJECT:([**DatabaseName**].[dbo].[MapStreetsPoints].[MapStreetsPoints4] AS [B]), SEEK:([B].[Longitude] >= CONVERT(decimal(8,5),[**DatabaseName**].[dbo].[BusStop].[XCoord]-(0.002),0) AND [B].[Longitude] <= CONVERT(decimal(8,5),[**DatabaseName**].[dbo].[BusStop].[XCoord]+(0.002),0)),  WHERE:([**DatabaseName**].[dbo].[MapStreetsPoints].[Latitude] as [B].[Latitude]>=CONVERT(decimal(8,5),[**DatabaseName**].[dbo].[BusStop].[YCoord]-(0.002),0) AND [**DatabaseName**].[dbo].[MapStreetsPoints].[Latitude] as [B].[Latitude]<=CONVERT(decimal(8,5),[**DatabaseName**].[dbo].[BusStop].[YCoord]+(0.002),0)) ORDERED FORWARD)
       |--Clustered Index Seek(OBJECT:([**DatabaseName**].[dbo].[MapStreets].[PK_MapStreets]), SEEK:([**DatabaseName**].[dbo].[MapStreets].[FeatureId]=[**DatabaseName**].[dbo].[MapStreetsPoints].[FeatureId] as [A].[FeatureId]) ORDERED FORWARD)

XML Version of showplan for multi-step version

多步骤版本的展示计划是:

    |--Table Insert(OBJECT:(@Temp), SET:([FeatureId] = [**DatabaseName**].[dbo].[MapStreetsPoints].[FeatureId] as [A].[FeatureId],[StartLongitude] = [**DatabaseName**].[dbo].[MapStreetsPoints].[Longitude] as [A].[Longitude],[StartLatitude] = [UDSD 2021
         |--Top(ROWCOUNT est 0)
              |--Nested Loops(Inner Join, OUTER REFERENCES:([B].[FeatureId], [Expr1013]))
                   |--Compute Scalar(DEFINE:([Expr1013]=[**DatabaseName**].[dbo].[MapStreetsPoints].[PointNumber] as [B].[PointNumber]-(1)))
                   |    |--Index Seek(OBJECT:([**DatabaseName**].[dbo].[MapStreetsPoints].[MapStreetsPoints4] AS [B]), SEEK:([B].[Longitude] >= CONVERT(decimal(8,5),[@Longitude]-(0.002),0) AND [B].[Longitude] <= CONVERT(decimal(8,5),[@Longitude]+(0.0
                   |--Index Seek(OBJECT:([**DatabaseName**].[dbo].[MapStreetsPoints].[MapStreetsPoints_PointNumber] AS [A]), SEEK:([A].[PointNumber]=[Expr1013] AND [A].[FeatureId]=[**DatabaseName**].[dbo].[MapStreetsPoints].[FeatureId] as [B].[FeatureI


    |--Table Insert(OBJECT:([**DatabaseName**].[dbo].[fnMapReverseGeocodeNearestStreet_2]), SET:([FeatureId] = @Temp.[FeatureId] as [T].[FeatureId]))
         |--Sort(TOP 1, ORDER BY:([Expr1005] ASC))
              |--Compute Scalar(DEFINE:([Expr1005]=[**DatabaseName**].[dbo].[PerpendicularDistanceToLine]([@Longitude],[@Latitude],@Temp.[StartLongitude] as [T].[StartLongitude],@Temp.[StartLatitude] as [T].[StartLatitude],@Temp.[EndLongitude] as [T]
                   |--Table Scan(OBJECT:(@Temp AS [T]))

我不明白为什么内联表值函数更慢。我最好的猜测是 order by 应用得太早了,因此在太多行上运行。应该提一下,应用过滤后,order by 可以查看的行通常少于 100 行。

PerpendicularDistanceToLine 是一个返回标量的多语句 UDF。它没有表访问权限,它只是对输入应用一系列数学运算。

【问题讨论】:

  • 我唯一能想到的可能是第一个版本中的索引减慢了排序速度?
  • 您真的使用的是 SQL 2005,还是标记错误?
  • 是的。实际上是 SQL Server 2005。我还有几个客户在使用 2005。(悲伤的脸)。
  • ORDER BY 中的标量 UDF 将定义缓慢。我建议您考虑将dbo.PerpendicularDistanceToLine itself 转换为内联 TVF,然后 CROSS APPLYing 并按此排序。您可能应该使用 GIS 数据类型,例如 geography,不确定 2005 年是否支持它
  • @MartinSmith - 我修改了问题以包含 showplan 的 XML 版本。

标签: sql sql-server sql-server-2005


【解决方案1】:

TLDR;

函数有一个谓词

Longitude BETWEEN XCoord-0.002 AND XCoord+0.002 
  AND Latitude BETWEEN YCoord-0.002 AND YCoord+0.002`

XCoordYCoord 是从外部表 (BusStop) 传入的。

对于内联表值函数,计划直接引用这些外部列,SQL Server 只是根据硬编码公式猜测返回的基数为6,557.1 每次执行并使用哈希联接。

对于多语句 TVF,这些值作为参数值传入。这允许它在实际第一次执行时嗅探传入的值并编译不同的计划。

不同的计划仍将仅基于一组参数值(未针对每个外部行单独优化),但在您的情况下,不同的计划可能总体上更好,它不仅可以减轻多语句 TVF 的开销自己。

内嵌 TVF 计划

多语句 TVF 计划(仅功能部分)

我最好的猜测是订单被应用得太早了 因此在太多行上运行。

不,事实并非如此。两个执行计划具有相同的基本策略。

  1. 对于 BusStop 中的每一行
    • a) 使用外行的相关参数计算 MapStreetsPoints 自身连接的结果
    • b) 评估连接结果上的标量 UDF 并执行 Top 1 排序并返回顶行。
  2. 然后针对从步骤 1 返回的每一行对 MapStreets 进行搜索

当然还有其他区别。

内联表值函数版本假脱机步骤 b 的结果,以防它看到相同的相关参数值(但可能每个公交车站都在一个唯一的位置,所以这在实践中永远不会发生)。

多语句 TVF 计划对 1a 和 1b 有单独的子树,它们被插入到两个不同的表变量中(1a 中的TOP 运算符在这里是“行数顶部”,以防执行计划在以下情况下使用SET ROWCOUNT 已设置并且根本不过滤行)。

尽管如此,在这两种情况下,标量 UDF 调用的数量和进入 TOP N 排序的行数应该相同,因为此步骤是根据步骤 1a 的结果运行的

不同的是步骤 1a 本身使用的连接策略。

内嵌 TVF 计划

  • 使用 Latitude BETWEEN BusStop.YCoord-(0.002) AND BusStop.YCoord+(0.002) 上的剩余谓词在 BusStop.XCoord-(0.002) AND BusStop.XCoord+(0.002) 之间的经度上查找。
  • 使用 Latitude BETWEEN BusStop.YCoord-(0.002) AND BusStop.YCoord+(0.002) 上的剩余谓词在 BusStop.XCoord-(0.002) AND BusStop.XCoord+(0.002) 之间的经度上查找
  • 使用哈希连接来连接A.FeatureId = B.FeatureId And A.PointNumber = B.PointNumber - 1 上的两个相同搜索

上面的搜索使用BETWEEN,其值从外行传入。 SQL Server 猜测每次执行将返回 16,557.1 行。我不确定确切的公式是什么,但这个估计完全是基于表基数的猜测。当我在我身边创建一个表并插入 2,044,080 行并使用旧的基数估计器时,我能够得到完全相同的猜测。

MultiStatement TVF 计划

  • 这会在 @Longitude-(0.002) AND @Longitude+(0.002) 之间的经度上进行搜索,并在 @Latitude-(0.002) 和 @Latitude+(0.002) 之间的纬度上进行剩余谓词。
  • 但它会执行嵌套循环并在 PointNumber, FeatureId 上具有相等条件,然后在 Latitude, Longitude 部分上查找范围的不同索引。

上面的初始搜索与散列连接情况中的相同。唯一的区别是谓词是否表示为外部表引用或函数参数,但估计的行完全不同。

您提供的计划是估计计划,并使用空参数值编译。你应该收集实际的计划并检查它是否相同。

假设计划相同,那么结论是嵌套循环实际上是比散列连接更好的示例数据连接选择。并且足以克服多语句 TVF 的额外包袱。

您应该尝试以下方法来获得相同的嵌套循环计划,但没有多语句包袱,看看它的执行情况。

CREATE FUNCTION dbo.fnMapReverseGeocodeNearestStreet_i2
(   
    @Longitude Decimal(8,5),
    @Latitude Decimal(8,5)
)
RETURNS TABLE 
AS
RETURN 
(
    Select  Top 1 
            A.FeatureId
    From    MapStreetsPoints As A
            Inner LOOP Join MapStreetsPoints As B with (index = [MapStreetsPoints_PointNumber])
                On A.FeatureId = B.FeatureId
                And A.PointNumber + 1 = B.PointNumber
    Where   A.Longitude Between     Convert(Decimal(8,5), @Longitude - 0.002) 
                                    and Convert(Decimal(8,5), @Longitude + 0.002)
            And A.Latitude Between  Convert(Decimal(8,5), @Latitude - 0.002)
                                    And Convert(Decimal(8,5), @Latitude + 0.002)
            And B.Longitude Between Convert(Decimal(8,5), @Longitude - 0.002) 
                                    and Convert(Decimal(8,5), @Longitude + 0.002)
            And B.Latitude Between  Convert(Decimal(8,5), @Latitude - 0.002)
                                    And Convert(Decimal(8,5), @Latitude + 0.002)
    Order By dbo.PerpendicularDistanceToLine(@Longitude, @Latitude, 
                A.Longitude, 
                A.Latitude, 
                B.Longitude, 
                B.Latitude)
)

您当然应该摆脱标量 UDF 并将其替换为内联 TVF,以摆脱与非内联标量 UDF 相关的开销

【讨论】:

  • 我衷心感谢您为此付出的努力来帮助我。我已经使用 SQL Server 20 年了(回到 sql 2000)。我以前从来不需要查询提示。当我运行您的查询版本时,它需要 13 秒,比多语句版本更快。非常感谢。我今天学到了很多。
  • @GeorgeMastros - 希望如果您将标量 UDF 替换为返回一行的内联表值函数,您可以进一步降低它。这里有一个例子stackoverflow.com/a/70518022/73226。显然,如果您可以鼓励您的客户迁移到具有 Geography 类型的版本,您就可以利用空间索引
  • 我将标量 UDF 变成了 iTVF。不幸的是,这让事情变得更慢了。我测试了原始 UDF 的性能。在我的系统上,运行大约需要 25 微秒。由于它只是针对一个充满记录的拳头,我确定这不是问题所在。再次感谢您的帮助。
猜你喜欢
  • 2016-05-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-05-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多