¿SQL Server lee toda una función COALESCE incluso si el primer argumento no es NULL?


98

Estoy usando una función T-SQL COALESCEdonde el primer argumento no será nulo en aproximadamente el 95% de las veces que se ejecuta. Si el primer argumento es NULL, el segundo argumento es un proceso bastante largo:

SELECT COALESCE(c.FirstName
                ,(SELECT TOP 1 b.FirstName
                  FROM TableA a 
                  JOIN TableB b ON .....)
                )

Si, por ejemplo, c.FirstName = 'John'¿SQL Server seguiría ejecutando la subconsulta?

Sé que con la IIF()función VB.NET , si el segundo argumento es True, el código sigue leyendo el tercer argumento (aunque no se usará).

Respuestas:


95

Nop . Aquí hay una prueba simple:

SELECT COALESCE(1, (SELECT 1/0)) -- runs fine
SELECT COALESCE(NULL, (SELECT 1/0)) -- throws error

Si se evalúa la segunda condición, se genera una excepción para dividir por cero.

Según la documentación de MSDN, esto está relacionado con la forma COALESCEen que el intérprete lo ve: es una manera fácil de escribir una CASEdeclaración.

CASE es bien conocido por ser una de las únicas funciones en SQL Server que (en su mayoría) cortocircuita de manera confiable.

Hay algunas excepciones al comparar con variables escalares y agregaciones como lo muestra Aaron Bertrand en otra respuesta aquí (y esto se aplicaría tanto a CASEy COALESCE):

DECLARE @i INT = 1;
SELECT CASE WHEN @i = 1 THEN 1 ELSE MIN(1/0) END;

generará una división por cero error.

Esto debería considerarse un error y, por regla general, COALESCEse analizará de izquierda a derecha.


66
@JNK, vea mi respuesta para ver un caso muy simple en el que esto no es cierto (mi preocupación es que hay aún más escenarios aún no descubiertos, lo que hace difícil acordar que CASEsiempre evalúe los circuitos de izquierda a derecha y siempre cortocircuitos )
Aaron Bertrand

44
Otro comportamiento interesante @SQLKiwi me señaló: SELECT COALESCE((SELECT CASE WHEN RAND() <= 0.5 THEN 1 END), 1);- repetir varias veces. Obtendrás a NULLveces. Inténtalo de nuevo con ISNULL- nunca obtendrás NULL...
Aaron Bertrand


@ Martin, sí, eso creo. Pero no es un comportamiento que la mayoría de los usuarios encontrarían intuitivo a menos que hayan escuchado (o hayan sido mordidos) por ese problema.
Aaron Bertrand

73

¿Qué tal este, según me informó Itzik Ben-Gan, a quien Jaime Lafargue le contó ?

DECLARE @i INT = 1;
SELECT CASE WHEN @i = 1 THEN 1 ELSE MIN(1/0) END;

Resultado:

Msg 8134, Level 16, State 1, Line 2
Divide by zero error encountered.

Hay soluciones triviales, por supuesto, pero el punto es que CASEno siempre garantiza una evaluación / cortocircuito de izquierda a derecha. Informé el error aquí y se cerró como "por diseño". Posteriormente, Paul White archivó este elemento Connect y se cerró como Fijo. No porque se haya solucionado per se, sino porque actualizaron Books Online con una descripción más precisa del escenario en el que los agregados pueden cambiar el orden de evaluación de una CASEexpresión. Recientemente escribí más sobre esto aquí .

EDITE solo un apéndice, aunque estoy de acuerdo en que estos son casos extremos, que la mayoría de las veces puede confiar en la evaluación de izquierda a derecha y en cortocircuito, y que estos son errores que contradicen la documentación y probablemente eventualmente se solucionarán ( esto no es definitivo: vea la conversación de seguimiento en la publicación del blog de Bart Duncan para ver por qué), tengo que estar en desacuerdo cuando la gente dice que algo siempre es cierto, incluso si hay un caso de borde único que lo refuta. Si Itzik y otros pueden encontrar errores solitarios como este, al menos es posible que también haya otros errores. Y dado que no conocemos el resto de la consulta del OP, no podemos decir con certeza que dependerá de este cortocircuito, pero terminará siendo mordido por él. Entonces, para mí, la respuesta más segura es:

Si bien generalmente puede confiar CASEpara evaluar el cortocircuito de izquierda a derecha, como se describe en la documentación, no es exacto decir que siempre puede hacerlo. Hay dos casos demostrados en esta página en los que no es cierto, y ninguno de los errores se ha corregido en ninguna versión pública de SQL Server.

EDITAR aquí es otro caso (necesito dejar de hacerlo) en el que una CASEexpresión no se evalúa en el orden que esperaría, aunque no haya agregados involucrados.


2
Y parece que hubo otro problema con CASE eso que se solucionó en silencio
Martin Smith el

En mi opinión, esto no prueba que la evaluación de la expresión CASE no esté garantizada porque los valores agregados se calculan antes de seleccionar (para que puedan usarse dentro de have).
Salman A

1
@SalmanA No estoy seguro de qué más puede hacer esto, excepto probar exactamente que el orden de evaluación en una expresión CASE no está garantizado. Recibimos una excepción porque el agregado se calcula primero, a pesar de que está en una cláusula ELSE que, si se sigue la documentación, nunca se debe alcanzar.
Aaron Bertrand

Los agregados de @AaronBertrand se calculan antes de la declaración CASE (y deberían OMI). La documentación revisada señala exactamente esto, que el error ocurre antes de que se evalúe CASE.
Salman A

@SalmanA Todavía demuestra al desarrollador casual que la expresión CASE no se evalúa en el orden en que se escribió: la mecánica subyacente es irrelevante si todo lo que intenta hacer es comprender por qué un error proviene de una rama CASE que no debería ' Ha sido alcanzado. ¿Tiene argumentos en contra de todos los otros ejemplos en esta página también?
Aaron Bertrand

37

Mi opinión sobre esto es que la documentación deja bastante claro que la intención es que CASE deba cortocircuitar. Como Aaron menciona, ha habido varios casos (¡ja!) En los que se ha demostrado que esto no siempre es cierto.

Hasta ahora, todos estos han sido reconocidos como errores y corregidos, aunque no necesariamente en una versión de SQL Server que puede comprar y parchear hoy (el error de plegado constante aún no ha llegado a una actualización acumulativa AFAIK). El nuevo error potencial, originalmente informado por Itzik Ben-Gan, aún no se ha investigado (Aaron o yo lo agregaremos a Connect en breve).

En relación con la pregunta original, hay otros problemas con CASE (y, por lo tanto, COALESCE) en los que se utilizan funciones de efecto secundario o subconsultas. Considerar:

SELECT COALESCE((SELECT CASE WHEN RAND() <= 0.5 THEN 999 END), 999);
SELECT ISNULL((SELECT CASE WHEN RAND() <= 0.5 THEN 999 END), 999);

El formulario COALESCE a menudo devuelve NULL, más detalles en https://connect.microsoft.com/SQLServer/feedback/details/546437/coalesce-subquery-1-may-return-null

Los problemas demostrados con las transformaciones del optimizador y el seguimiento de expresiones comunes significan que es imposible garantizar que CASE se cortocircuite en todas las circunstancias. Puedo concebir casos en los que tal vez ni siquiera sea posible predecir el comportamiento al inspeccionar el resultado del plan de exhibición pública, aunque hoy no tengo una réplica para eso.

En resumen, creo que puede estar razonablemente seguro de que CASE provocará un cortocircuito en general (particularmente si una persona razonablemente experta inspecciona el plan de ejecución, y ese plan de ejecución se 'aplica' con una guía de plan o sugerencias) pero si necesita una garantía absoluta, debe escribir SQL que no incluya la expresión en absoluto.

No es un estado de cosas enormemente satisfactorio, supongo.


18

Me he encontrado con otro caso donde CASE/ COALESCEno cortocircuito. El siguiente TVF generará una infracción de PK si se pasa 1como parámetro.

CREATE FUNCTION F (@P INT)
RETURNS @T TABLE (
  C INT PRIMARY KEY)
AS
  BEGIN
      INSERT INTO @T
      VALUES      (1),
                  (@P)

      RETURN
  END

Si se llama de la siguiente manera

DECLARE @Number INT = 1

SELECT COALESCE(@Number, (SELECT number
                          FROM   master..spt_values
                          WHERE  type = 'P'
                                 AND number = @Number), 
                         (SELECT TOP (1)  C
                          FROM   F(@Number))) 

O como

DECLARE @Number INT = 1

SELECT CASE
         WHEN @Number = 1 THEN @Number
         ELSE (SELECT TOP (1) C
               FROM   F(@Number))
       END 

Ambos dan el resultado

Infracción de la restricción PRIMARY KEY 'PK__F__3BD019A800551192'. No se puede insertar una clave duplicada en el objeto 'dbo. @ T'. El valor clave duplicado es (1).

mostrando que la SELECT(o al menos la población variable de la tabla) todavía se lleva a cabo y genera un error a pesar de que nunca se debe alcanzar esa rama de la declaración. El plan para la COALESCEversión está abajo.

Plan

Esta reescritura de la consulta parece evitar el problema

SELECT COALESCE(Number, (SELECT number
                          FROM   master..spt_values
                          WHERE  type = 'P'
                                 AND number = Number), 
                         (SELECT TOP (1)  C
                          FROM   F(Number))) 
FROM (VALUES(1)) V(Number)   

Que da plan

Plan2


8

Otro ejemplo

CREATE TABLE T1 (C INT PRIMARY KEY)

CREATE TABLE T2 (C INT PRIMARY KEY)

INSERT INTO T1 
OUTPUT inserted.* INTO T2
VALUES (1),(2),(3);

La consulta

SET STATISTICS IO ON;

SELECT T1.C,
       COALESCE(T1.C , CASE WHEN EXISTS (SELECT * FROM T2 WHERE T2.C = T1.C)  THEN -1 END)
FROM T1
OPTION (LOOP JOIN)

No muestra lecturas en contra T2en absoluto.

La búsqueda de T2está bajo un predicado de paso y el operador nunca se ejecuta. Pero

SELECT T1.C,
       COALESCE(T1.C , CASE WHEN EXISTS (SELECT * FROM T2 WHERE T2.C = T1.C)  THEN -1 END)
FROM T1
OPTION (MERGE JOIN)

Hace espectáculo que T2se lee. A pesar de T2que nunca se necesita ningún valor de .

Por supuesto, esto no es realmente sorprendente, pero pensé que valía la pena agregarlo al repositorio de ejemplos de contador solo porque plantea el problema de lo que significa cortocircuito incluso en un lenguaje declarativo basado en conjuntos.


7

Solo quería mencionar una estrategia que quizás no hayas considerado. Puede que no coincida aquí, pero a veces resulta útil. Vea si esta modificación le brinda un mejor rendimiento:

SELECT COALESCE(c.FirstName
            ,(SELECT TOP 1 b.FirstName
              FROM TableA a 
              JOIN TableB b ON .....
              WHERE C.FirstName IS NULL) -- this is the changed part
            )

Otra forma de hacerlo podría ser esto (básicamente equivalente, pero le permite acceder a más columnas de la otra consulta si es necesario):

SELECT COALESCE(c.FirstName, x.FirstName)
FROM
   TableC c
   OUTER APPLY (
      SELECT TOP 1 b.FirstName
      FROM
         TableA a 
         JOIN TableB b ON ...
      WHERE
         c.FirstName IS NULL -- the important part
   ) x

Básicamente, esta es una técnica de unión "dura" de tablas, pero que incluye la condición de cuándo se debe unir cualquier fila. En mi experiencia, esto realmente ha ayudado a los planes de ejecución a veces.


3

No, no lo haría. Solo se ejecutará cuando c.FirstNamesea NULL.

Sin embargo, deberías probarlo tú mismo. Experimentar. Dijiste que tu subconsulta es larga. Punto de referencia. Saca tus propias conclusiones sobre esto.

La respuesta de @Aaron en la subconsulta que se ejecuta es más completa.

Sin embargo, sigo pensando que debería volver a trabajar su consulta y usarla LEFT JOIN. La mayoría de las veces, las subconsultas pueden eliminarse modificando su consulta para usar LEFT JOINs.

El problema con el uso de subconsultas es que su declaración general se ejecutará más lentamente porque la subconsulta se ejecuta para cada fila en el conjunto de resultados de la consulta principal.


@ Adrian todavía no está bien. Mire el plan de ejecución y verá que las subconsultas a menudo se convierten de manera inteligente en JOIN. Es un simple error de experimento mental suponer que la subconsulta completa debe ejecutarse una y otra vez para cada fila, aunque esto puede suceder efectivamente si se elige una unión de bucle anidado con un escaneo.
ErikE

3

El estándar real dice que todas las cláusulas WHEN (así como la cláusula ELSE) deben analizarse para determinar el tipo de datos de la expresión como un todo. Realmente tendría que sacar algunas de mis notas anteriores para determinar cómo se maneja un error. Pero de la mano, 1/0 usa números enteros, por lo que supongo que si bien es un error. Es un error con el tipo de datos entero. Cuando solo tiene nulos en la lista de fusión, es un poco más complicado determinar el tipo de datos, y ese es otro problema.

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.