2016-06-30 10 views
6

Я получаю нечетные результаты при использовании NEWID() в сочетании с постоянным вычисленным столбцом. Я использую какую-то функцию неправильно?Непоследовательные результаты с вычисляемым столбцом NEWID() и PERSISTED

Не использовать сохранение при создании столбца и, следовательно, вычисление значений при их выборе, вернет правильные значения. Обновление столбца (col1) также вернет правильные значения.

DECLARE @test TABLE (
    Col1 INT, 
    Contains2 AS CASE WHEN 2 IN (Col1) THEN 1 ELSE 0 END PERSISTED) 

INSERT INTO @test (Col1) VALUES 
    (ABS(CHECKSUM(NEWID()) % 5)), 
    (ABS(CHECKSUM(NEWID()) % 5)), 
    (ABS(CHECKSUM(NEWID()) % 5)), 
    (ABS(CHECKSUM(NEWID()) % 5)), 
    (ABS(CHECKSUM(NEWID()) % 5)) 

SELECT * FROM @test 
UPDATE @test SET Col1 = Col1*1 
SELECT * FROM @test 

/* 
Col1 Contains2 
2 0 
2 0 
0 1 
4 0 
3 0 

Col1 Contains2 
2 1 
2 1 
0 0 
4 0 
3 0 
*/ 
+0

Я думаю, что стоит отметить, что ожидаемое поведение с ключевым словом 'PERSISTED' опущено. Возможно, вы захотите назвать это в своем вопросе. –

+0

@DanGuzman Хороший вопрос, обновленный вопрос. – Kristofer

+2

перекрестная ссылка на [dba.se] (http://dba.stackexchange.com/q/142675/68127) –

ответ

4

По-видимому, механизм запроса вычисляет случайное число дважды для каждой строки.

Первый раз за Col1, второй раз за CASE заявление о сохраненной колонке.

Оптимизатор не знает или не заботится в этом случае, что NEWID является недетерминированной функцией и вызывает его дважды.

Собственно, у него может даже не быть выбора. Вы хотите, чтобы оптимизатор создавал временную таблицу за кулисами, заполнял ее Col1 результатами выражения, которое генерирует случайные числа, затем читает эту временную таблицу и использует эти сохраненные результаты посредника для вычисления результата выражения CASE, а затем выполняет окончательный INSERT? В этом случае оптимизатор дешевле вычислять выражение дважды, не записывая промежуточные результаты на диск. В некоторых других случаях (например, если у вас нет 5, но 5 миллиардов строк или дополнительных индексов), расчетные затраты могут быть разными, и это поведение изменится.

Я не думаю, что вы можете много сделать. Просто знайте об этом поведении. Всегда явно сохранять сгенерированный набор случайных чисел в таблицу, а затем выполнять дальнейшие вычисления на их основе.

Я воспроизвел его в SQL Server 2008 и 2014 году. Вот план выполнения, который я получил в SQL Server 2008, но это не очень интересно. В 2014 году план одинаков, кроме оператора Top.

plan 2008

Constant Scan оператор выводит Union1009 список, который используется в Compute Scalar позже. Я думаю, это сводится к деталям реализации Constant Scan и/или Compute Scalar операторам.

Наблюдаемое поведение говорит нам, что newid() вызывается здесь дважды.

+0

Странно, план выполнения показывает, что случайные значения вычисляются только один раз. Они вычисляются как часть постоянного сканирования. После этого вычислительный скаляр вычислял вычисленный столбец. – usr

+0

@usr, я вижу оператор 'Constant scan', а затем' Compute Scalar' для вычисления 'CASE', который использует вывод из' Constant scan'.Я не вижу явно в плане, что результаты оператора 'Constant scan' хранятся где-то в памяти и не пересчитываются по мере необходимости. В любом случае наблюдаемое поведение говорит нам, что 'NEWID()' вызывается дважды в строке. –

+0

Я согласен, что это проблема. Это просто не видно в плане. Кто-то с глубоким внутренним знанием, вероятно, может объяснить, как это реализовано. Обычно скаляры вычисляются лениво и один раз. Для этого не нужна таблица temp. – usr

1

Во время тестирования я удалил функции, не связанные с NEWID, и показал результаты, если NEWID были рассчитаны заранее. Это может быть полезно для других.

DECLARE @test TABLE (
InsertType VARCHAR(30), 
Col1 VARCHAR(5), 
Contains2 AS CASE WHEN (Col1) LIKE '%2%' THEN 1 ELSE 0 END) --depends on Col1 

INSERT INTO @test (InsertType, Col1) VALUES 
    ('Compute With Insert', LEFT(NEWID(), 5)), 
    ('Compute With Insert', LEFT(NEWID(), 5)), 
    ('Compute With Insert', LEFT(NEWID(), 5)), 
    ('Compute With Insert', LEFT(NEWID(), 5)), 
    ('Compute With Insert', LEFT(NEWID(), 5)) 

SELECT * FROM @test 

DECLARE @A VARCHAR(5) = LEFT(NEWID(), 5); 
DECLARE @B VARCHAR(5) = LEFT(NEWID(), 5); 
DECLARE @C VARCHAR(5) = LEFT(NEWID(), 5); 
DECLARE @D VARCHAR(5) = LEFT(NEWID(), 5); 
DECLARE @E VARCHAR(5) = LEFT(NEWID(), 5); 

SELECT @A, @B, @C, @D, @E; 

INSERT INTO @Test (InsertType, Col1) VALUES 
('Compute Before Insert', @A), ('Compute Before Insert', @B), ('Compute Before Insert', @C), ('Compute Before Insert', @D), ('Compute Before Insert', @E) 

SELECT * FROM @test 

InsertType     Col1  Contains2 
Compute With Insert  C5507  0 
Compute With Insert  C17D7  0 
Compute With Insert  D9087  1 
Compute With Insert  E2DB0  0 
Compute With Insert  7D1AF  1 
Compute Before Insert  31050  0 
Compute Before Insert  2954C  1 
Compute Before Insert  9E205  1 
Compute Before Insert  DDF05  0 
Compute Before Insert  ED708  0