SQL Server 优化器删除 ISNULL 调用

标签 sql sql-server t-sql

以下查询在 SQL Server 2017 中遇到错误:

;with locations(RowNum, Latitude, Longitude) as (
    select 1, 12.3456, 45.6789
),
locationsWithPrevious as (
    select  *,
            PreviousLatitude = lag(l.Latitude) over(order by l.RowNum),
            PreviousLongitude = lag(l.Longitude) over(order by l.RowNum)
    from    locations as l
),
locationsWithDistance as (
    select  *,
            Distance = geography::Point(l.Latitude, l.Longitude, 4326).STDistance(geography::Point(l.PreviousLatitude, l.PreviousLongitude, 4326))
    from    locationsWithPrevious as l
    where   PreviousLatitude is not null
        and PreviousLongitude is not null
)
select  *
from    locationsWithDistance as l
where   Distance > 0

Msg 6569, Level 16, State 1, Line 1
'geography::Point' failed because parameter 1 is not allowed to be null.

原因: 谓词 Distance > 0 在将 PreviousLatitude/-Longitude 过滤为 IS NOT NULL 之前执行。 到目前为止一切顺利,因为 T-SQL 是声明性的,并且这里的操作顺序可以由 SQL Server 确定。 如果删除谓词 Distance > 0,查询将正常运行,不会出现错误。

但现在我希望可以通过使用 ISNULL 函数来防止参数的 NULL 值,如下所示:

Distance = geography::Point(l.Latitude, l.Longitude, 4326).STDistance(geography::Point(isnull(l.PreviousLatitude, 0), isnull(l.PreviousLongitude, 0), 4326))

但是查询仍然返回相同的错误! ISNULL 函数也没有在执行计划的过滤谓词中的任何位置列出!

SQL Server 的这种行为正确吗? 在我看来,由于 IS NOT NULL 过滤,SQL Server 错误地删除了 ISNULL 调用。

注释:

  1. 删除 IS NOT NULL 条件后,错误就会消失,因为现在按预期在过滤谓词中使用了 ISNULL 函数(但查询已当然,语义上发生了变化):

     locationsWithDistance as (
         select  *,
                 Distance = geography::Point(l.Latitude, l.Longitude, 4326).STDistance(geography::Point(isnull(l.PreviousLatitude, 0), isnull(l.PreviousLongitude, 0), 4326))
         from    locationsWithPrevious as l
     )
    
  2. 但是,如果您将 ISNULL 调用替换为 CASE WHEN 操作,则查询可以正常工作:

     Distance = geography::Point(l.Latitude, l.Longitude, 4326).STDistance(geography::Point(case when l.PreviousLatitude is not null then l.PreviousLatitude else 0 end, case when l.PreviousLongitude is not null then l.PreviousLongitude else 0 end, 4326))
    
  3. 我还意识到,通过直接在基本查询中实例化 Point,可以更好地制定查询,如下所示:

     ;with locations(RowNum, GeoPosition) as (
         select 1, geography::Point(12.3456, 45.6789, 4326)
     ),
     locationsWithPrevious as (
         select  *,
                 PreviousGeoPosition = lag(l.GeoPosition) over(order by l.RowNum)
         from    locations as l
     ),
     locationsWithDistance as (
         select  *,
                 Distance = l.GeoPosition.STDistance(l.PreviousGeoPosition)
         from    locationsWithPrevious as l
         where   PreviousGeoPosition is not null
     )
     select  *
     from    locationsWithDistance as l
     where   Distance > 0
    

最佳答案

这确实是一个错误。编译器认为该值可证明不为空,并删除了 ISNULL。但是,COALESCE 不会以同样的方式受到影响,它会编译为 CASE 并且编译器对其没有太多可见性。

编译器将中间计算放入计算标量运算符中。但这些可以计算 at different points by the Expression Service ,因此 ISNULL 不应该被删除。

正如您所发现的,一种解决方法是删除 WHERE

另一种方法是使用 LAG 上的额外参数来添加默认值

;with locations(RowNum, Latitude, Longitude) as (
    select 1, 12.3456, 45.6789
),
locationsWithPrevious as (
    select  *,
            PreviousLatitude = lag(l.Latitude, 1, 0) over(order by l.RowNum),
            PreviousLongitude = lag(l.Longitude, 1, 0) over(order by l.RowNum)
    from    locations as l
),
locationsWithDistance as (
    select  *,
            Distance = geography::Point(l.Latitude, l.Longitude, 4326).STDistance(geography::Point(l.PreviousLatitude, l.PreviousLongitude, 4326))
    from    locationsWithPrevious as l
)
select  *
from    locationsWithDistance as l
where   Distance > 0

db<>fiddle

关于SQL Server 优化器删除 ISNULL 调用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/69941734/

相关文章:

sql - 从科学计数法到 yyyy/mm/dd hh :mm:ss 的 SAS 时间戳

sql-server - 如何在两个服务器之间共享全局变量

sql - 使用别名的 ORDER BY 中的 Postgres CASE

sql - 在play2中哪里可以看到记录的sql语句?

sql - 需要在 SQL Server 2012 中自动递增字符串

MySQL order by date 奇怪的问题

sql-server - 无法连接到家庭网络上的 SQL Server 2019

sql-server - TSQL程序求最大素数

sql - 执行SQL遇到的问题

sql - 如何深度复制一组数据,并将 FK 引用更改为指向所有副本?