¿Es legal que SQL Server complete columnas PERSISTADAS con datos que no coinciden con la definición?

16

Estoy siguiendo esta pregunta sobre valores extraños en una PERSISTEDcolumna calculada. La respuesta allí hace algunas conjeturas sobre cómo surgió este comportamiento.

Estoy preguntando lo siguiente: ¿No es esto un error absoluto? ¿Se PERSISTEDles permite a las columnas comportarse de esta manera?

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

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 --shows impossible data

UPDATE @test SET Col1 = Col1*1 --"fix" the data by rewriting it

SELECT * FROM @test --observe fixed data

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

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

Tenga en cuenta que los datos parecen "imposibles" porque los valores de la columna calculada no corresponden a su definición.

Es bien sabido que las funciones no deterministas en las consultas pueden comportarse de manera extraña, pero aquí esto parece violar el contrato de las columnas computadas persistentes y, por lo tanto, debería ser ilegal.

Insertar números aleatorios podría ser un escenario artificial, pero ¿qué pasaría si insertáramos NEWID()valores o SYSUTCDATETIME()? Creo que este es un tema relevante que prácticamente podría manifestarse.

usr
fuente

Respuestas:

9

Esto es ciertamente un error. El hecho de que los col1valores sean el resultado de una expresión que involucra números aleatorios claramente no cambia cuál col2se supone que es el valor correcto . DBCC CHECKDBdevuelve un error si se ejecuta contra una tabla permanente.

create table test (
    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));

DBCC CHECKDB

Da (para mi ejecución de prueba que tenía una fila "imposible")

Msg 2537, Level 16, State 106, Line 17
Table error: object ID 437576597, index ID 0, partition ID 72057594041008128, alloc unit ID 72057594046251008 (type In-row data), page (1:121), row 0. The record check (valid computed column) failed. The values are 2 and 0.
DBCC results for 'test'.
There are 5 rows in 1 pages for object "test".
CHECKDB found 0 allocation errors and 1 consistency errors in table 'test' (object ID 437576597).

También informa que

repair_allow_data_loss es el nivel mínimo de reparación para los errores encontrados por DBCC CHECKDB

Y si se utiliza la opción de reparación, elimina sin ceremonias toda la fila, ya que no tiene forma de saber qué columna está dañada.

Adjuntar un depurador muestra que NEWID()se está evaluando dos veces por fila insertada. Una vez antes de CASEevaluar la expresión y una vez dentro de ella.

ingrese la descripción de la imagen aquí

Una posible solución podría ser usar

INSERT INTO @test
            (Col1)
SELECT ( ABS(CHECKSUM(NEWID()) % 5) )
FROM   (VALUES (1),(1),(1),(1),(1)) V(X); 

Lo que por una razón u otra evita el problema y solo evalúa la expresión una vez por fila.

Martin Smith
fuente
2

Según la conversación de comentarios, el consenso parece ser que la respuesta a la pregunta del OP es que esto constituye un error (es decir, debería ser ilegal).

El OP hace referencia al análisis de Vladimir Baranov sobre StackOverflow, donde afirman:

"Primera vez para Col1, segunda vez para la declaración CASE de la columna persistente.

Optimizer no sabe, o no le importa en este caso que NEWID es una función no determinista y la llama dos veces ".

Dicho de otra manera, se debe esperar que [NEWID () dentro de] col1 tenga el mismo valor que acaba de insertar como cuando realiza el cálculo.

Esto sería sinónimo de lo que está sucediendo con el error, donde NEWID se crea para Col1 y luego se crea nuevamente para la columna persistente:

INSERT INTO @Test (Col1, Contains2) VALUES
(NEWID(), CASE WHEN (NEWID()) LIKE '%2%' THEN 1 ELSE 0 END)

En mis pruebas, otras funciones no deterministas como RAND y valores de tiempo no dieron como resultado el mismo error.

Según Martin, esto se ha planteado a Microsoft ( https://connect.microsoft.com/SQLServer/Feedback/Details/2751288 ) donde hay comentarios a esta página y al análisis de StackOverflow (a continuación).

John
fuente