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$1257927703
tabla (en el ejemplo) siempre tendrá una cantidad mayor o igual al número de filas que la dbf_1162761$z$dd$1257927703
tabla, 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)
fuente
Respuestas:
Como regla general, SQL Server ejecutará las partes de una
CASE
declaración en orden, pero es libre de reordenar lasOR
condiciones. Para algunas consultas, puede obtener un rendimiento consistentemente mejor cambiando el orden de lasWHEN
expresiones dentro de unaCASE
declaración. A veces también puede obtener un mejor rendimiento al cambiar el orden de las condiciones en unaOR
declaració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:
Considere la siguiente consulta:
Sabemos que evaluar la subconsulta
X_CI
será mucho más barato que la subconsultaX_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:
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
: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á:
La siguiente consulta es mucho menos eficiente:
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:
Los resultados de rendimiento lo respaldan:
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:
Y en esta consulta se evalúan en el orden opuesto:
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
CASE
instrucción o algún otro método para forzar el pedido. De lo contrario, siéntase libre de pedir subconsultas en laOR
condició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:
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_HEAP
no 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.fuente
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 END
podrí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.