SQL Server divide A <> B en A <B OR A> B, produciendo resultados extraños si B no es determinista


26

Hemos encontrado un problema interesante con SQL Server. Considere el siguiente ejemplo de repro:

CREATE TABLE #test (s_guid uniqueidentifier PRIMARY KEY);
INSERT INTO #test (s_guid) VALUES ('7E28EFF8-A80A-45E4-BFE0-C13989D69618');

SELECT s_guid FROM #test
WHERE s_guid = '7E28EFF8-A80A-45E4-BFE0-C13989D69618'
  AND s_guid <> NEWID();

DROP TABLE #test;

violín

Por favor, olvide por un momento que la s_guid <> NEWID()condición parece completamente inútil, este es solo un ejemplo mínimo de repro. Dado que la probabilidad de NEWID()igualar un valor constante dado es extremadamente pequeña, debe evaluar VERDADERO cada vez.

Pero no lo hace. La ejecución de esta consulta generalmente devuelve 1 fila, pero a veces (con bastante frecuencia, más de 1 de cada 10 veces) devuelve 0 filas. Lo he reproducido con SQL Server 2008 en mi sistema, y ​​puede reproducirlo en línea con el violín vinculado anteriormente (SQL Server 2014).

Al observar el plan de ejecución, se revela que el analizador de consultas aparentemente divide la condición en s_guid < NEWID() OR s_guid > NEWID():

captura de pantalla del plan de consulta

... lo que explica completamente por qué falla a veces (si la primera ID generada es más pequeña y la segunda más grande que la ID dada).

¿Se permite que SQL Server evalúe A <> Bcomo A < B OR A > B, incluso si una de las expresiones no es determinista? En caso afirmativo, ¿dónde está documentado? ¿O encontramos un error?

Curiosamente, AND NOT (s_guid = NEWID())produce el mismo plan de ejecución (y el mismo resultado aleatorio).

Encontramos este problema cuando un desarrollador quería excluir opcionalmente una fila en particular y usaba:

s_guid <> ISNULL(@someParameter, NEWID())

como un "atajo" para:

(@someParameter IS NULL OR s_guid <> @someParameter)

Estoy buscando documentación y / o confirmación de un error. El código no es tan relevante, por lo que no se requieren soluciones alternativas.


Respuestas:


22

¿Se permite que SQL Server evalúe A <> Bcomo A < B OR A > B, incluso si una de las expresiones no es determinista?

Este es un punto algo controvertido, y la respuesta es un "sí" calificado.

La mejor discusión que conozco se dio en respuesta al informe de error Connect de Itzik Ben-Gan Error con NEWID y Expresiones de tabla , que se cerró porque no se solucionará. Connect se ha retirado desde entonces, por lo que el enlace está a un archivo web. Lamentablemente, la desaparición de Connect perdió mucho material útil (o lo hizo más difícil de encontrar). De todos modos, las citas más útiles de Jim Hogg de Microsoft son:

Esto llega al meollo del problema: ¿se permite la optimización para cambiar la semántica de un programa? Es decir: si un programa produce ciertas respuestas, pero se ejecuta lentamente, ¿es legítimo que un Optimizador de consultas haga que ese programa se ejecute más rápido, pero también cambie los resultados dados?

Antes de gritar "¡NO!" (mi propia inclinación personal también :-), considere: la buena noticia es que, en el 99% de los casos, las respuestas SON las mismas. Entonces, la optimización de consultas es una clara victoria. La mala noticia es que, si la consulta contiene un código de efectos secundarios, los diferentes planes PUEDEN producir resultados diferentes. Y NEWID () es una de esas 'funciones' de efectos secundarios (no deterministas) que expone la diferencia. [En realidad, si experimenta, puede idear otros, por ejemplo, evaluación de cortocircuito de cláusulas AND: hacer que la segunda cláusula arroje una división aritmética por cero; diferentes optimizaciones pueden ejecutar esa segunda cláusula ANTES de la primera cláusula] Esto refleja La explicación de Craig, en otra parte de este hilo, que SqlServer no garantiza cuando se ejecutan operadores escalares.

Entonces, tenemos una opción: si queremos garantizar un cierto comportamiento en presencia de un código no determinista (efecto secundario), de modo que los resultados de JOIN, por ejemplo, sigan la semántica de una ejecución de bucle anidado, entonces puede usar las OPCIONES apropiadas para forzar ese comportamiento, como lo señala UC. Pero el código resultante se ejecutará lentamente: ese es el costo de, en efecto, obstaculizar el Optimizador de consultas.

Dicho todo esto, estamos moviendo el Optimizador de consultas en la dirección del comportamiento "según lo esperado" para NEWID (), intercambiando el rendimiento por "resultados según lo esperado".

Un ejemplo del cambio de comportamiento a este respecto a lo largo del tiempo es que NULLIF funciona incorrectamente con funciones no deterministas como RAND () . También hay otros casos similares que se usan, por ejemplo, COALESCEcon una subconsulta que puede producir resultados inesperados y que también se están abordando gradualmente.

Jim continúa:

Cerrando el ciclo . . . He discutido esta pregunta con el equipo de desarrollo. Y finalmente hemos decidido no cambiar el comportamiento actual, por las siguientes razones:

1) El optimizador no garantiza el tiempo ni el número de ejecuciones de funciones escalares. Este es un principio largamente establecido. Es el 'margen de maniobra' fundamental que le da al optimizador suficiente libertad para obtener mejoras significativas en la ejecución del plan de consulta.

2) Este "comportamiento de una vez por fila" no es un problema nuevo, aunque no se discute ampliamente. Comenzamos a modificar su comportamiento en el lanzamiento de Yukon. ¡Pero es bastante difícil precisar con precisión, en todos los casos, exactamente lo que significa! Por ejemplo, ¿se aplica a las filas intermedias calculadas 'en el camino' al resultado final? - en cuyo caso, depende claramente del plan elegido. ¿O se aplica solo a las filas que finalmente aparecerán en el resultado completado? - Hay una desagradable recursión aquí, ¡estoy seguro de que estarás de acuerdo!

3) Como mencioné anteriormente, por defecto "optimizamos el rendimiento", lo cual es bueno para el 99% de los casos. El 1% de los casos en los que podría cambiar los resultados es bastante fácil de detectar ('funciones' de efectos secundarios como NEWID) y fácil de 'corregir' (rendimiento comercial, como consecuencia). Este valor predeterminado para "optimizar el rendimiento" nuevamente, está establecido desde hace mucho tiempo y es aceptado. (Sí, no es la postura elegida por los compiladores para los lenguajes de programación convencionales, pero que así sea).

Entonces, nuestras recomendaciones son:

a) Evite la confianza en el tiempo no garantizado y la semántica del número de ejecuciones. b) Evite usar NEWID () en las expresiones de tabla. c) Use OPTION para forzar un comportamiento particular (rendimiento comercial)

Espero que esta explicación ayude a aclarar nuestras razones para cerrar este error, ya que "no se solucionará".


Curiosamente, AND NOT (s_guid = NEWID())produce el mismo plan de ejecución

Esto es una consecuencia de la normalización, que ocurre muy temprano durante la compilación de consultas. Ambas expresiones se compilan exactamente en la misma forma normalizada, por lo que se produce el mismo plan de ejecución.


En este caso, si queremos forzar un plan particular que parece evitar el problema, podemos usar WITH (FORCESCAN). Para estar seguros, debemos usar una variable para almacenar el resultado de NEWID () antes de ejecutar la consulta.
Razvan Socol

11

Esto está documentado (más o menos) aquí:

El número de veces que una función especificada en una consulta se ejecuta realmente puede variar entre los planes de ejecución creados por el optimizador. Un ejemplo es una función invocada por una subconsulta en una cláusula WHERE. El número de veces que se ejecuta la subconsulta y su función puede variar con las diferentes rutas de acceso elegidas por el optimizador.

Funciones definidas por el usuario

Este no es el único formulario de consulta donde el plan de consulta ejecutará NEWID () varias veces y cambiará el resultado. Esto es confuso, pero en realidad es crítico para que NEWID () sea útil para la generación de claves y la ordenación aleatoria.

Lo más confuso es que no todas las funciones no deterministas se comportan realmente de esta manera. Por ejemplo, RAND () y GETDATE () se ejecutarán solo una vez por consulta.


¿Hay alguna publicación de blog o similar que explique por qué / cuándo el motor convertirá "no es igual" en un rango?
Señor Magoo

3
No que yo sepa. Puede ser rutinario porque =, <y >puede ser evaluado eficientemente contra un BTree.
David Browne - Microsoft

5

Para lo que vale, si observa este antiguo documento estándar SQL 92 , los requisitos sobre desigualdad se describen en la sección " 8.2 <comparison predicate>" de la siguiente manera:

1) Deje que X e Y sean dos elementos <constructor de valor de fila> s correspondientes. Deje que XV e YV sean los valores representados por X e Y, respectivamente.

[...]

ii) "X <> Y" es verdadero si y solo si XV e YV no son iguales.

[...]

7) Deje que Rx y Ry sean los dos <constructores de valores de fila> s del <predicado de comparación> y deje que RXi y RYi sean el i-ésimo <elemento constructor de valores de fila> s de Rx y Ry, respectivamente. "Rx <comp op> Ry" es verdadero, falso o desconocido de la siguiente manera:

[...]

b) "x <> Ry" es verdadero si y solo si RXi <> RYi para algunos i.

[...]

h) "x <> Ry" es falso si y solo si "Rx = Ry" es verdadero.

Nota: Incluí 7b y 7h para completar, ya que hablan de <>comparación: no creo que la comparación de constructores de valores de fila con múltiples valores se implemente en T-SQL, a menos que solo esté malentendiendo enormemente lo que esto dice, lo cual es bastante posible

Este es un montón de basura confusa. Pero si quieres seguir buceando en el basurero ...

Yo creo que 1.ii es el elemento que se aplica en este escenario, ya que estamos comparando los valores de "elementos de valor de fila de constructores."

ii) "X <> Y" es verdadero si y solo si XV e YV no son iguales.

Básicamente dice que X <> Yes cierto si los valores representados por X e Y no son iguales. Dado que X < Y OR X > Yes una reescritura lógicamente equivalente de ese predicado, es totalmente genial que el optimizador lo use.

El estándar no impone restricciones en esta definición relacionadas con la determinismo (o lo que sea, lo obtienes) de los elementos del constructor de valores de fila a cada lado del <>operador de comparación. Es responsabilidad del código de usuario lidiar con el hecho de que una expresión de valor en un lado podría no ser determinista.


1
Me abstendré de votar (arriba o abajo) pero no estoy convencido. Las citas que proporciona mencionan "valor" . Tengo entendido que la comparación es entre dos valores, uno en cada lado. No entre dos (o más) instancias de un valor en cada lado. Además, el estándar (al menos el 92 que cita) no menciona en absoluto las funciones no deterministas. Por un razonamiento similar al suyo, podemos suponer que un producto SQL que cumple con el estándar no proporciona ninguna función no determinista sino solo las mencionadas en el estándar.
ypercubeᵀᴹ

@yper gracias por los comentarios! Creo que tu interpretación es definitivamente válida. Esta es la primera vez que leo ese documento. Está mencionando valores en el contexto del valor representado por un "constructor de valores de fila", que en otra parte del documento que dice puede ser una subconsulta escalar (entre muchas otras cosas). La subconsulta escalar en particular parece que podría ser no determinista. Pero realmente no sé de qué estoy hablando =)
Josh Darnell
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.