【问题标题】:Why is my SQL Server 2017 query returning incorrect results?为什么我的 SQL Server 2017 查询返回不正确的结果?
【发布时间】:2019-02-15 23:24:46
【问题描述】:

以下是我遇到的问题的一些重现代码。

在 SQL SERVER 2017 中运行它,与任何其他 SQL SERVER 版本相比,您将得到不同(且不正确)的结果将数据库设置为 sql Server 2017 实例上的较低兼容性级别,它也可以正常工作。

为什么会发生这种情况,如何在不更改兼容性级别的情况下修复它?

实际结果

+--------------+--------------+----------------+---------+-----------+---------+------------+-------+
| IsPriorAfter | IsIdealAfter | IsCurrentAfter | IsPrior | IsCurrent | IsIdeal | SecurityID | PosID |
+--------------+--------------+----------------+---------+-----------+---------+------------+-------+
|            1 |            1 |              1 |       1 |         1 |       1 |        123 |     1 |
|            0 |            0 |              0 |       0 |         1 |       1 |        234 |     2 |
|            0 |            0 |              0 |       1 |         0 |       0 |        234 |     3 |
+--------------+--------------+----------------+---------+-----------+---------+------------+-------+

预期结果

+--------------+--------------+----------------+---------+-----------+---------+------------+-------+
| IsPriorAfter | IsIdealAfter | IsCurrentAfter | IsPrior | IsCurrent | IsIdeal | SecurityID | PosID |
+--------------+--------------+----------------+---------+-----------+---------+------------+-------+
|            1 |            1 |              1 |       1 |         1 |       1 |        123 |     1 |
|            0 |            1 |              1 |       0 |         1 |       1 |        234 |     2 |
|            1 |            0 |              0 |       1 |         0 |       0 |        234 |     3 |
+--------------+--------------+----------------+---------+-----------+---------+------------+-------+

复制

if object_id('ForSubQuery') is not null begin
    DROP TABLE ForSubQuery
end
Create Table ForSubQuery
(   
    SecID int
)

INSERT INTO ForSubQuery SELECT 123
INSERT INTO ForSubQuery SELECT 234

GO

SELECT * FROM ForSubQuery

if object_id('MainTable') is not null begin
    DROP TABLE MainTable
end
Create Table MainTable
(   
    IsPrior bit,
    IsCurrent bit,
    IsIdeal bit,
    [SecurityID] int,
    PosID int
)

INSERT INTO MainTable SELECT 1,1,1,123,1
INSERT INTO MainTable SELECT 0,1,1,234,2
INSERT INTO MainTable SELECT 1,0,0,234,3

GO

SELECT * FROM MainTable

SELECT 
       CASE
            WHEN
                Position.IsPrior = 1
                AND Position.[SecurityID] in (SELECT
                SecID
                FROM ForSubQuery
                )               
                 THEN 1
            ELSE 0
        END AS IsPriorAfter
       ,CASE
            WHEN
                Position.IsIdeal = 1
                AND [Position].[SecurityID] IN (SELECT
                        secid
                FROM ForSubQuery            
                    )
                 THEN 1
            ELSE 0
        END AS IsIdealAfter 
     ,CASE
            WHEN
                Position.IsCurrent = 1
                AND [Position].[SecurityID] IN (SELECT
                        secid
                FROM ForSubQuery
                    )
                 THEN 1
            ELSE 0
        END AS IsCurrentAfter
    , Position.*
    FROM MainTable [Position]
    order by Position.PosID

【问题讨论】:

  • 问题是多个case语句中存在同一个子查询,它不再起作用了
  • 无法重现:dbfiddle.uk/…
  • 请发帖SELECT @@version 我建议安装最新的CU-13

标签: sql-server sql-server-2017


【解决方案1】:

TLDR

这是a bug that has been fixed in CU8,所以至少安装那个 CU,最好安装最新的。

SQL Server 2017 前版

在 SQL Server 2016 中,计划如上所示。 IN 的处理方式与 EXISTS 相同,因此它会评估以下三列。

   CASE WHEN IsPrior = 1   AND EXISTS (SELECT * FROM ForSubQuery WHERE SecID = MainTable.SecurityID) THEN 1 ELSE 0 END AS IsPriorAfter
   CASE WHEN IsIdeal = 1   AND EXISTS (SELECT * FROM ForSubQuery WHERE SecID = MainTable.SecurityID) THEN 1 ELSE 0 END AS IsIdealAfter
   CASE WHEN IsCurrent = 1 AND EXISTS (SELECT * FROM ForSubQuery WHERE SecID = MainTable.SecurityID) THEN 1 ELSE 0 END AS IsCurrentAfter

每个子查询实例在计划中都有自己的运算符,查询返回正确的结果,但这是次优的,因为相同的子查询可能每行执行最多 3次。

因为每个子查询旁边都有一个AND,但是如果该表达式的结果为假,SQL Server 可以跳过评估子查询。这是通过每个包含直通谓词的嵌套循环来实现的。例如,对应于IsPriorAfter 的评估具有IsFalseOrNull (IsPrior=1) 的直通谓词

IsPrior=1 是一个布尔表达式,可以返回falsenulltrue。然后IsFalseOrNull 反转结果并返回1falsenull0true。因此,如果IsPrior 不是1(包括NULL),则传递谓词计算为true/1,然后将跳过执行子查询。

SQL Server 2017 RTM

SQL Server 2017 引入了新的优化规则CollapseIdenticalScalarSubquery。在RTM版本中执行计划不正确。

问题计划

子查询现在在单个运算符中,并且通过谓词组合在一起

IsFalseOrNull([IsCurrent]=(1)) OR IsFalseOrNull([IsIdeal]=(1)) OR IsFalseOrNull([IsPrior]=(1))

但是这个条件是不正确的!它评估为true,除非IsPriorIsIdealIsCurrent 中的所有三个是1

因此,在您的情况下,子查询仅执行一次(对于表中的第一行 - 所有三列都等于 1)。

对于另外两行,它应该被执行但不是。如果相关子查询返回一行,则嵌套循环有一个探测列,该列设置为1。 (在计划中标记为Expr1016)。跳过执行时,此探测列设置为 NULL

计划中的最终计算标量具有以下表达式。当Expr1016null 时,对于使用CASE 计算的所有三个列,这将计算为0

[Expr1005] = Scalar Operator(CASE WHEN [IsPrior]=(1) AND [Expr1016] THEN (1) ELSE (0) END), 
[Expr1009] = Scalar Operator(CASE WHEN [IsIdeal]=(1) AND [Expr1016] THEN (1) ELSE (0) END), 
[Expr1013] = Scalar Operator(CASE WHEN [IsCurrent]=(1) AND [Expr1016] THEN (1) ELSE (0) END)

SQL Server 2017 已修补

应用 CU 后的最终固定计划与 2017 年的 RTM 计划具有相同的计划形状(子查询仅出现一次)但通过谓词现在是

IsFalseOrNull([IsCurrent]=(1)) AND IsFalseOrNull([IsIdeal]=(1)) AND IsFalseOrNull([IsPrior]=(1))

仅当这些列中没有的值为 1 时,才会评估为 true,因此现在可以在需要时准确评估子查询。

【讨论】:

  • 非常感谢您的详细解释。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-07-25
  • 1970-01-01
  • 1970-01-01
  • 2018-12-22
相关资源
最近更新 更多