Optimizar selección en subconsulta con COALESCE (...)

8

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 StaffNamesección, y que estoy seleccionando de la vista en adelante StaffName. Si cambio eso pe.StaffName AS StaffNameao 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 JOINpodrí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 COALESCEcual es muy rápido.
  • Para mi sorpresa, mirar el plan de ejecución no levanta ninguna bandera, incluso cuando WHEREse incluye la subconsulta de ajuste y la declaración. Mi costo total de subconsulta en el analizador es 0.0065736. Hmph Tarda cuatro segundos en ejecutarse.
  • Cambiar la aplicación para consultar de manera diferente (por ejemplo, regresar pe.StaffName AS PEStaffName, se.StaffName AS SEStaffNamey hacer WHERE PEStaffName = 'X' OR SEStaffName = 'X') podría funcionar, pero como último recurso, realmente espero poder optimizar la vista sin tener que recurrir a tocar la aplicación.
  • 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 ) Zanterior, 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 COALESCEy 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 StaffNamerealidad 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 COALESCEs que estoy haciendo de todos modos. Probándolo ahora.

S'pht'Kr
fuente
¿Qué índices has agregado? ¿Estás incluyendo el StaffName en el índice?
Mark Sinkinson
@ MarkSinkinson Tengo un índice no agrupado en cada tabla KeyField, ambos indexan INCLUDEel StaffNamecampo 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.
S'pht'Kr
1
Tiene la WHERE pe.ThisThing = 1 AND se.OtherThing = 0condición que cancela la FULL OUTERunión y hace que la consulta sea equivalente a una unión interna. ¿Estás seguro de que necesitas una unión COMPLETA?
ypercubeᵀᴹ
@ypercube Lo siento, eso fue una mala codificación aérea de mi parte, el punto es más que tengo condiciones en ambas tablas, pero sí, tengo en cuenta los nulos en ambos lados en la consulta real. Estoy fusionando las dos tablas y buscando coincidencias, pero necesito los datos disponibles de cualquiera de las tablas cuando no hay un registro coincidente a la izquierda o la derecha, así que sí, necesito la combinación completa.
S'pht'Kr
1
Un pensamiento: es una posibilidad remota, pero se puede tratar de romper la consulta interna en tres partes ( INNER JOIN, LEFT JOINcon WHERE IS NULLcheque, RIGHT JOIN con IS NULL) y luego UNION ALLlas tres partes. De esta forma no habrá necesidad de usar COALESCE()y podría (solo podría) ayudar al optimizador a descubrir la reescritura.
ypercubeᵀᴹ

Respuestas:

4

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 JOINcon WHERE IS NULLverificación, RIGHT JOINcon IS NULLverificación) y luego UNION ALLlas tres partes. Esto tiene las siguientes ventajas:

  • El optimizador tiene menos opciones de transformación disponibles para FULLcombinaciones que para (las más comunes) INNERy LEFTcombinaciones.

  • La Ztabla 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 la INNERparte unirse.

  • Mejora menor, el uso COALESCE()será mínimo, si es que lo hay (supuse que se.SEIdy pe.PEIdno son anulables. Si más columnas no son anulables, podrá eliminar más COALESCE()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:

SELECT 
    se.SEId + '-' + pe.PEId AS Id,
    se.SEId, pe.PEId,
    pe.StaffName, 
    pe.EventTime,
    COALESCE(pe.EventType, se.EventType) AS EventType,
    pe.Duration,
    COALESCE(pe.Data, se.Data) AS Data,
    COALESCE(pe.Field, se.Field) AS Field,
    pe.ThisThing, se.OtherThing,
    DATEADD(minute, pe.Duration, pe.EventTime) AS EventEndTime
FROM PE pe INNER 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) 

UNION ALL

SELECT 
    '0-0',
    NULL, pe.PEId,
    pe.StaffName, 
    pe.EventTime,
    pe.EventType,
    pe.Duration,
    pe.Data,
    pe.Field,
    pe.ThisThing, NULL,
    DATEADD(minute, pe.Duration, pe.EventTime) AS EventEndTime
FROM PE pe LEFT 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.StaffName IS NULL

UNION ALL

SELECT 
    '0-0',
    se.SEId, NULL,
    se.StaffName, 
    se.EventTime,
    se.EventType,
    se.Duration,
    se.Data,
    se.Field,
    NULL, se.OtherThing, 
    DATEADD(minute, se.Duration, se.EventTime) AS EventEndTime
FROM PE pe RIGHT JOIN SE se 
  ON pe.StaffName = se.StaffName
 AND pe.Duration = se.Duration
 AND pe.EventTime = se.EventTime
WHERE NOT (se.OtherThing = 0)
  AND pe.StaffName IS NULL ;
ypercubeᵀᴹ
fuente
0

Mi intuición sería que esto no debería ser un problema, ya que para cuando COALESCE(pe.StaffName, se.StaffName) AS StaffNamehace 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).

David Spillett
fuente
Gracias David, he estado errando por el lado de la paranoia en cuanto a cuánto debería revelar sobre esto incluso en cuanto a la estructura (pe => PatientEvent, así que ...) pero sé que eso lo hace más difícil. Creo que, de hecho, está haciendo la unión basada en índices y luego haciendo una "comparación simple en memoria" para filtrar ... pero la tabla derivada sin filtrar Zactualmente regresa con ~ 1.5m de filas. Lo que quiero que haga es reescribir ese predicado en la consulta para Zque 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.
S'pht'Kr