orden de las cláusulas en "EXISTE (...) O EXISTE (...)"

11

Tengo una clase de consultas que prueban la existencia de una de dos cosas. Es de la forma

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM ...)
  OR EXISTS (SELECT 1 FROM ...)
THEN 1 ELSE 0 END;

La declaración real se genera en C y se ejecuta como una consulta ad-hoc a través de una conexión ODBC.

Recientemente ha salido a la luz que el segundo SELECT probablemente será más rápido que el primer SELECT en la mayoría de los casos y que cambiar el orden de las dos cláusulas EXISTS causó una aceleración drástica en al menos un caso de prueba abusivo que acabamos de crear.

Lo más obvio es seguir adelante y cambiar las dos cláusulas, pero quería ver si alguien más familiarizado con SQL Server se preocuparía por evaluar esto. Parece que estoy confiando en la coincidencia y en un "detalle de implementación".

(También parece que si SQL Server fuera más inteligente, ejecutaría ambas cláusulas EXISTS en paralelo y permitiría que una completara primero un cortocircuito a la otra).

¿Hay una mejor manera de hacer que SQL Server mejore constantemente el tiempo de ejecución de dicha consulta?

Actualizar

Gracias por su tiempo e interés en mi pregunta. No esperaba preguntas sobre los planes de consulta reales, pero estoy dispuesto a compartirlas.

Esto es para un componente de software que admite SQL Server 2008R2 y superior. La forma de los datos puede ser bastante diferente según la configuración y el uso. Mi compañero de trabajo pensó en hacer este cambio en la consulta porque la dbf_1162761$z$rv$1257927703tabla (en el ejemplo) siempre tendrá una cantidad mayor o igual al número de filas que la dbf_1162761$z$dd$1257927703tabla, a veces significativamente más (órdenes de magnitud).

Aquí está el caso abusivo que mencioné. La primera consulta es lenta y tarda unos 20 segundos. La segunda consulta se completa en un instante.

Por lo que vale, el bit "OPTIMIZAR PARA DESCONOCIDO" también se agregó recientemente porque la detección de parámetros estaba destruyendo ciertos casos.

Consulta original:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
  OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)

Plan original:

|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
     |--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
          |--Constant Scan
          |--Concatenation
               |--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
               |    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
               |    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)
               |--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
                    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]),  WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
                    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]),  WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)

Consulta fija:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
  OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)

Plan fijo:

|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
     |--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
          |--Constant Scan
          |--Concatenation
               |--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
               |    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]),  WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
               |    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]),  WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)
               |--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
                    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
                    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)
jr
fuente
1
Preguntas y respuestas relacionadas: Operación física de concatenación: ¿garantiza el orden de ejecución?
Paul White 9

Respuestas:

11

Como regla general, SQL Server ejecutará las partes de una CASEdeclaración en orden, pero es libre de reordenar las ORcondiciones. Para algunas consultas, puede obtener un rendimiento consistentemente mejor cambiando el orden de las WHENexpresiones dentro de una CASEdeclaración. A veces también puede obtener un mejor rendimiento al cambiar el orden de las condiciones en una ORdeclaración, pero no se garantiza el comportamiento.

Probablemente sea mejor recorrerlo con un ejemplo simple. Estoy probando con SQL Server 2016, por lo que es posible que no obtenga exactamente los mismos resultados en su máquina, pero que yo sepa, se aplican los mismos principios. Primero pondré un millón de enteros del 1 al 1000000 en dos tablas, una con un índice agrupado y otra como un montón:

CREATE TABLE dbo.X_HEAP (ID INT NOT NULL, FLUFF VARCHAR(100));

INSERT INTO dbo.X_HEAP  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

CREATE TABLE dbo.X_CI (ID INT NOT NULL, FLUFF VARCHAR(100), PRIMARY KEY (ID));

INSERT INTO dbo.X_CI  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

Considere la siguiente consulta:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;

Sabemos que evaluar la subconsulta X_CIserá mucho más barato que la subconsulta X_HEAP, especialmente cuando no hay una fila coincidente. Si no hay una fila coincidente, solo necesitamos hacer algunas lecturas lógicas en la tabla con un índice agrupado. Sin embargo, necesitaríamos escanear todas las filas del montón para saber que no hay una fila coincidente. El optimizador también lo sabe. En términos generales, usar un índice agrupado para buscar una fila es muy barato en comparación con el escaneo de una tabla.

Para este ejemplo de datos, escribiría la consulta de esta manera:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
ELSE 0 END;

Eso efectivamente obliga a SQL Server a ejecutar la subconsulta contra la tabla con un índice agrupado primero. Aquí están los resultados de SET STATISTICS IO, TIME ON:

Tabla 'X_CI'. Cuenta de escaneo 0, lecturas lógicas 3, lecturas físicas 0

Tiempos de ejecución de SQL Server: tiempo de CPU = 0 ms, tiempo transcurrido = 0 ms.

Mirando el plan de consulta, si la búsqueda en la etiqueta 1 devuelve algún dato, el escaneo en la etiqueta 2 no es obligatorio y no sucederá:

buena consulta

La siguiente consulta es mucho menos eficiente:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
ELSE 0 END
OPTION (MAXDOP 1);

Mirando el plan de consulta, vemos que el escaneo en la etiqueta 2 siempre ocurre. Si se encuentra una fila, se omite la búsqueda en la etiqueta 1. Ese no es el orden que queríamos:

mal plan de consulta

Los resultados de rendimiento lo respaldan:

Tabla 'X_HEAP'. Escaneo recuento 1, lecturas lógicas 7247

Tiempos de ejecución de SQL Server: tiempo de CPU = 15 ms, tiempo transcurrido = 22 ms.

Volviendo a la consulta original, para esta consulta veo la búsqueda y el escaneo evaluados en el orden que es bueno para el rendimiento:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;

Y en esta consulta se evalúan en el orden opuesto:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
THEN 1 ELSE 0 END;

Sin embargo, a diferencia del par de consultas anteriores, no hay nada que obligue al optimizador de consultas de SQL Server a evaluar una antes que la otra. No debe confiar en ese comportamiento para nada importante.

En conclusión, si necesita evaluar una subconsulta antes que la otra, use una CASEinstrucción o algún otro método para forzar el pedido. De lo contrario, siéntase libre de pedir subconsultas en la ORcondición que desee, pero sepa que no hay garantía de que el optimizador las ejecute en el orden tal como está escrito.

Apéndice:

Una pregunta de seguimiento natural es ¿qué puede hacer si desea que SQL Server decida qué consulta es más barata y ejecute esa primero? Todos los métodos hasta ahora parecen implementados por SQL Server en el orden en que se escribe la consulta, incluso si no se garantiza el comportamiento de algunos de ellos.

Aquí hay una opción que parece funcionar para las tablas de demostración simples:

SELECT CASE
  WHEN EXISTS (
    SELECT 1
    FROM (
        SELECT TOP 2 1 t
        FROM 
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_HEAP 
            WHERE ID = 50000 
        ) h
        CROSS JOIN
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_CI
            WHERE ID = 50000
        ) ci
    ) cnt
    HAVING COUNT(*) = 2
)
THEN 1 ELSE 0 END;

Puede encontrar una demostración de db fiddle aquí . Cambiar el orden de las tablas derivadas no cambia el plan de consulta. En ambas consultas X_HEAPno se toca la tabla. En otras palabras, el optimizador de consultas parece ejecutar primero la consulta más barata. No puedo recomendar el uso de algo como esto en producción, por lo que está aquí por valor de curiosidad. Puede haber una manera mucho más simple de lograr lo mismo.

Joe Obbish
fuente
44
O CASE WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000 UNION ALL SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 ELSE 0 ENDpodría ser una alternativa, aunque eso todavía depende de decidir manualmente qué consulta es más rápida y ponerla primero. No estoy seguro de si hay una manera de expresarlo para que SQL Server se reordene automáticamente, de modo que el barato se evalúe automáticamente primero.
Martin Smith