Почему мой запрос SQL Server 2017 возвращает неверные результаты? - PullRequest
0 голосов
/ 16 февраля 2019

Ниже приведен некоторый код repro для проблемы, с которой я столкнулся.

Запустите его в 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 |
+--------------+--------------+----------------+---------+-----------+---------+------------+-------+

Repro

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

1 Ответ

0 голосов
/ 16 февраля 2019

TLDR

Это ошибка, которая была исправлена ​​в CU8 , поэтому установка хотя бы этого CU и, в идеале, самого последнего исправит его.

Pre SQL Server 2017

enter image description here

В 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

Каждый экземпляр подзапроса получает свой собственный оператор в плане, и запрос возвращает правильный результат, но это subОптимальный, поскольку идентичный подзапрос может быть выполнен от до три раза в строке.

Поскольку каждый подзапрос имеет AND рядом с ним, SQL Server может пропустить оценку подзапроса, если результатом этого выражения является ложь.Это достигается каждым вложенным циклом, содержащим предикат прохода.Например, выражение, соответствующее оценке IsPriorAfter, имеет предикат сквозного доступа IsFalseOrNull (IsPrior=1)

IsPrior=1 - логическое выражение, которое может возвращать false, null или true.IsFalseOrNull затем инвертирует результат и возвращает 1 для false, null и 0 для true.Таким образом, предикат сквозной передачи оценивается как true / 1, если IsPrior отличается от 1 (включая NULL), и затем пропускает выполнение подзапроса.

SQLRTM Server 2017

В SQL Server 2017 введено новое правило оптимизации CollapseIdenticalScalarSubquery.В RTM-версии план выполнения неверен.

План задач

enter image description here

Подзапростеперь в одном операторе и сквозных предикатах объединяются

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

Однако это условие неверно!Он оценивается как true, если все три из IsPrior, IsIdeal, IsCurrent не равны 1.

Таким образом, в вашем случае подзапрос выполняется только один раз (для первой строки таблицы - где все три столбца равны 1).

Для двух других строк этодолжен быть выполнен, но это не так.Вложенные циклы имеют столбец зонда, который устанавливается на 1, если коррелированный подзапрос возвращает строку.(Помечено Expr1016 в плане).Когда выполнение пропускается, для этого столбца зонда устанавливается значение NULL

. Окончательный вычисляемый скаляр в плане имеет следующее выражение.Если Expr1016 равно null, это означает 0 для всех трех ваших вычисляемых столбцов с использованием CASE.

[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 имеет ту же форму плана, что и план RTM на 2017 год (с подзапросом, появляющимся только один раз), но предикат сквозной передачи теперь равен

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

Это оценивается только true если нет из этих столбцов имеют значение 1, поэтому подзапрос теперь вычисляется точно при необходимости.

...