Tengo una vista amplia que uso desde una aplicación. Creo que he reducido mi problema de rendimiento, pero no estoy seguro de cómo solucionarlo. Una versión simplificada de la vista se ve así:
SELECT ISNULL(SEId + '-' + PEId, '0-0') AS Id,
*,
DATEADD(minute, Duration, EventTime) AS EventEndTime
FROM (
SELECT se.SEId, pe.PEId,
COALESCE(pe.StaffName, se.StaffName) AS StaffName, -- << Problem!
COALESCE(pe.EventTime, se.EventTime) AS EventTime,
COALESCE(pe.EventType, se.EventType) AS EventType,
COALESCE(pe.Duration, se.Duration) AS Duration,
COALESCE(pe.Data, se.Data) AS Data,
COALESCE(pe.Field, se.Field) AS Field,
pe.ThisThing, se.OtherThing
FROM PE pe FULL OUTER JOIN SE se
ON pe.StaffName = se.StaffName
AND pe.Duration = se.Duration
AND pe.EventTime = se.EventTime
WHERE NOT(pe.ThisThing = 1 AND se.OtherThing = 0)
) Z
Probablemente eso no justifique la razón completa de la estructura de la consulta, pero tal vez le dé una idea: esta vista se une a dos tablas muy mal diseñadas sobre las que no tengo control e intenta sintetizar cierta información.
Entonces, dado que esta es una vista utilizada desde la aplicación, mientras trato de optimizar, la envuelvo en otro SELECT, así:
SELECT * FROM (
-- … above code …
) Q
WHERE StaffName = 'SMITH, JOHN Q'
porque la aplicación está buscando miembros del personal específicos en el resultado.
El problema parece ser la COALESCE(pe.StaffName, se.StaffName) AS StaffName
sección, y que estoy seleccionando de la vista en adelante StaffName
. Si cambio eso pe.StaffName AS StaffName
ao se.StaffName AS StaffName
, los problemas de rendimiento desaparecen (pero vea la actualización 2 a continuación) . Pero eso no funcionará porque FULL OUTER JOIN
podría faltar un lado u otro , por lo que uno u otro campo puede ser NULL.
¿Puedo refactorizar esto reemplazando el COALESCE(…)
con algo más, que se reescribirá en la subconsulta?
Otras notas:
- Ya he agregado algunos índices para solucionar problemas de rendimiento con el resto de la consulta, sin la
COALESCE
cual es muy rápido. - Para mi sorpresa, mirar el plan de ejecución no levanta ninguna bandera, incluso cuando
WHERE
se incluye la subconsulta de ajuste y la declaración. Mi costo total de subconsulta en el analizador es0.0065736
. Hmph Tarda cuatro segundos en ejecutarse. - Cambiar la aplicación para consultar de manera diferente
(por ejemplo, regresarpodría funcionar, pero como último recurso, realmente espero poder optimizar la vista sin tener que recurrir a tocar la aplicación.pe.StaffName AS PEStaffName, se.StaffName AS SEStaffName
y hacerWHERE PEStaffName = 'X' OR SEStaffName = 'X'
) - Un procedimiento almacenado probablemente tendría más sentido para esto, pero la aplicación está construida con Entity Framework, y no pude descubrir cómo hacer que funcione bien con un SP que devuelve un tipo de tabla (otro tema por completo).
Índices
Los índices que he agregado hasta ahora se parecen a esto:
CREATE NONCLUSTERED INDEX [IX_PE_EventTime]
ON [dbo].[PE] ([EventTime])
INCLUDE ([StaffName],[Duration],[EventType],[Data],[Field],[ThisThing])
CREATE NONCLUSTERED INDEX [IX_SE_EventTime]
ON [dbo].[SE] ([EventTime])
INCLUDE ([StaffName],[Duration],[EventType],[Data],[Field],[OtherThing])
Actualizar
Hmm ... Intenté simular el cambio afectado arriba, y no sirvió de nada. Es decir, antes de lo ) Z
anterior, agregué AND (pe.StaffName = 'SMITH, JOHN Q' OR se.StaffName = 'SMITH, JOHN Q')
, pero el rendimiento es el mismo. Ahora realmente no sé por dónde empezar.
Actualización 2
El comentario de @ypercube sobre la necesidad de la unión completa me hizo darme cuenta de que mi consulta sintetizada dejaba fuera un componente probablemente importante. Si bien, sí, necesito la combinación completa, la prueba que hice anteriormente al soltar COALESCE
y probar solo un lado de la combinación para obtener un valor no nulo haría que el otro lado de la combinación completa sea irrelevante , y el optimizador probablemente estaba usando esto hecho para acelerar la consulta. Además, he actualizado el ejemplo para mostrar que en StaffName
realidad es una de las claves de combinación, lo que probablemente tenga una relación significativa con la pregunta. Ahora también me inclino hacia su sugerencia de que dividir esto en una unión de tres vías en lugar de una unión completa puede ser la respuesta, y simplificará la abundancia de COALESCE
s que estoy haciendo de todos modos. Probándolo ahora.
fuente
KeyField
, ambos indexanINCLUDE
elStaffName
campo y varios otros campos. Puedo publicar las definiciones de índice en la pregunta. Estoy trabajando en esto en un servidor de prueba para poder agregar cualquier índice que creas que podría ser útil.WHERE pe.ThisThing = 1 AND se.OtherThing = 0
condición que cancela laFULL OUTER
unión y hace que la consulta sea equivalente a una unión interna. ¿Estás seguro de que necesitas una unión COMPLETA?INNER JOIN
,LEFT JOIN
conWHERE IS NULL
cheque, RIGHT JOIN con IS NULL) y luegoUNION ALL
las tres partes. De esta forma no habrá necesidad de usarCOALESCE()
y podría (solo podría) ayudar al optimizador a descubrir la reescritura.Respuestas:
Esto fue bastante arriesgado, pero como el OP dice que funcionó, lo estoy agregando como respuesta (siéntase libre de corregirlo si encuentra algo mal).
Intente dividir la consulta interna en tres partes (
INNER JOIN
,LEFT JOIN
conWHERE IS NULL
verificación,RIGHT JOIN
conIS NULL
verificación) y luegoUNION ALL
las tres partes. Esto tiene las siguientes ventajas:El optimizador tiene menos opciones de transformación disponibles para
FULL
combinaciones que para (las más comunes)INNER
yLEFT
combinaciones.La
Z
tabla derivada se puede eliminar (puede hacerlo de todos modos) de la definición de vista.El
NOT(pe.ThisThing = 1 AND se.OtherThing = 0)
será necesaria sólo en laINNER
parte unirse.Mejora menor, el uso
COALESCE()
será mínimo, si es que lo hay (supuse quese.SEId
ype.PEId
no son anulables. Si más columnas no son anulables, podrá eliminar másCOALESCE()
llamadas).Más importante, el optimizador puede reducir cualquier condición en sus consultas que involucran estas columnas (ahora eso
COALESCE()
no está bloqueando el empuje)Todo lo anterior le dará al optimizador más opciones para transformar / reescribir cualquier consulta que use la vista para que pueda encontrar un plan de ejecución que pueda usar índices en las tablas subyacentes.
En total, la vista se puede escribir como:
fuente
Mi intuición sería que esto no debería ser un problema, ya que para cuando
COALESCE(pe.StaffName, se.StaffName) AS StaffName
hace algo, todas las filas de las dos fuentes ya deberían haberse incorporado y emparejado, por lo que la llamada a la función es una comparación simple en memoria y nula -recoger. Obviamente, este no es el caso, por lo que quizás algo en una de las fuentes (si son vistas o tablas derivadas en línea) o en las tablas base (es decir, falta de índices) está haciendo que el planificador de consultas piense que necesita escanear estas columnas por separado.Sin más detalles de la consulta exacta que está ejecutando, las estructuras de soporte y los planes de consulta producidos, todo lo que sugerimos es una conjetura.
Para intentar forzar la comparación para que se realice después de todo lo demás, puede intentar seleccionar ambos valores en la tabla derivada (
pe.StaffName AS pe.StaffName, se.StaffName AS seStaffName
) y luego hacer la selección en la consulta externa (COALESCE(peStaffName, seStaffName) AS StaffName
), o incluso podría insertar los datos de la consulta interna en una tabla temporal luego realiza la consulta externa seleccionando de eso (pero eso requeriría un procedimiento almacenado, y dependiendo del número de filas este volcado a tempdb podría ser costoso y, por lo tanto, problemático en sí mismo).fuente
Z
actualmente regresa con ~ 1.5m de filas. Lo que quiero que haga es reescribir ese predicado en la consulta paraZ
que use los índices ... pero ahora también estoy confundido porque cuando coloco el predicado manualmente, todavía no usa un índice ... así que ahora No estoy seguro.