La consulta no responde al agregar dos columnas

9

Cuando agrego dos columnas a mi selección, la consulta no responde. El tipo de columna es nvarchar(2000). Es un poco inusual.

  • La versión de SQL Server es 2014.
  • Solo hay un índice primario.
  • Los registros completos son solo 1000 filas.

Aquí está el plan de ejecución antes (plan de presentación XML ):

ingrese la descripción de la imagen aquí

Plan de ejecución después (plan de presentación XML ):

ingrese la descripción de la imagen aquí

Aquí está la consulta:

select top(100)
  Batch_Tasks_Queue.id,
  btq.id,
  Batch_Tasks_Queue.[Parameters], -- this field
  btq.[Parameters]  -- and this field
from
        Batch_Tasks_Queue with(nolock)
    inner join  Batch_Tasks_Queue btq with(nolock)  on  Batch_Tasks_Queue.Start_Time < btq.Start_Time
                            and btq.Start_Time < Batch_Tasks_Queue.Finish_Time
                            and Batch_Tasks_Queue.id <> btq.id                            
                            and btq.Start_Time is not null
                            and btq.State in (3, 4)                          
where
    Batch_Tasks_Queue.Start_Time is not null      
    and Batch_Tasks_Queue.State in (3, 4)
    and Batch_Tasks_Queue.Operation_Type = btq.Operation_Type
    and Batch_Tasks_Queue.Operation_Type not in (23, 24, 25, 26, 27, 28, 30)

order by
    Batch_Tasks_Queue.Start_Time desc

El resultado total es de 17 filas. Los datos sucios (pista nolock) no son importantes.

Aquí está la estructura de la tabla:

CREATE TABLE [dbo].[Batch_Tasks_Queue](
    [Id] [int] NOT NULL,
    [OBJ_VERSION] [numeric](8, 0) NOT NULL,
    [Operation_Type] [numeric](2, 0) NULL,
    [Request_Time] [datetime] NOT NULL,
    [Description] [varchar](1000) NULL,
    [State] [numeric](1, 0) NOT NULL,
    [Start_Time] [datetime] NULL,
    [Finish_Time] [datetime] NULL,
    [Parameters] [nvarchar](2000) NULL,
    [Response] [nvarchar](max) NULL,
    [Billing_UserId] [int] NOT NULL,
    [Planned_Start_Time] [datetime] NULL,
    [Input_FileId] [uniqueidentifier] NULL,
    [Output_FileId] [uniqueidentifier] NULL,
    [PRIORITY] [numeric](2, 0) NULL,
    [EXECUTE_SEQ] [numeric](2, 0) NULL,
    [View_Access] [numeric](1, 0) NULL,
    [Seeing] [numeric](1, 0) NULL,
 CONSTRAINT [PKBachTskQ] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [Batch_Tasks_QueueData]
) ON [Batch_Tasks_QueueData] TEXTIMAGE_ON [Batch_Tasks_QueueData]
GO    
SET ANSI_PADDING OFF
GO
ALTER TABLE [dbo].[Batch_Tasks_Queue]  WITH NOCHECK ADD  CONSTRAINT [FK0_BtchTskQ_BlngUsr] FOREIGN KEY([Billing_UserId])
REFERENCES [dbo].[BILLING_USER] ([ID])
GO
ALTER TABLE [dbo].[Batch_Tasks_Queue] CHECK CONSTRAINT [FK0_BtchTskQ_BlngUsr]
GO
Hamid Fathi
fuente
La discusión sobre esta pregunta se ha trasladado a esta sala de chat .
Paul White 9

Respuestas:

15

Resumen

Los principales problemas son:

  • La selección del plan del optimizador supone una distribución uniforme de valores.
  • La falta de índices adecuados significa:
    • Escanear la tabla es la única opción.
    • La unión es una unión de bucles anidados ingenua , en lugar de una unión de bucles anidados de índice . En una unión ingenua, los predicados de unión se evalúan en la unión en lugar de ser empujados hacia abajo por el lado interno de la unión.

Detalles

Los dos planes son fundamentalmente bastante similares, aunque el rendimiento puede ser muy diferente:

Plan con las columnas adicionales

Tomando el que tiene las columnas adicionales que no se completa en un tiempo razonable primero:

Plan lento

Las características interesantes son:

  1. La parte superior en el nodo 0 limita las filas devueltas a 100. También establece un objetivo de fila para el optimizador, por lo que todo lo que está debajo de él en el plan se elige para devolver las primeras 100 filas rápidamente.
  2. La exploración en el nodo 4 encuentra filas de la tabla donde Start_Timeno es nulo, Statees 3 o 4 y Operation_Typees uno de los valores enumerados. La tabla se escanea completamente una vez, y cada fila se prueba con los predicados mencionados. Solo las filas que pasan todas las pruebas fluyen a la Clasificación. El optimizador estima que 38,283 filas calificarán.
  3. La ordenación en el nodo 3 consume todas las filas de la exploración en el nodo 4 y las ordena en orden de Start_Time DESC. Este es el orden de presentación final solicitado por la consulta.
  4. El optimizador estima que habrá que leer 93 filas (en realidad 93.2791) de la Clasificación para que todo el plan devuelva 100 filas (lo que representa el efecto esperado de la unión).
  5. Se espera que la unión de bucles anidados en el nodo 2 ejecute su entrada interna (la rama inferior) 94 veces (en realidad 94.2791). El intercambio de paralelismo de parada en el nodo 1 requiere una fila adicional por razones técnicas.
  6. La exploración en el nodo 5 explora completamente la tabla en cada iteración. Encuentra filas donde Start_Timeno es nulo y Statees 3 o 4. Se estima que esto produce 400.875 filas en cada iteración. En 94.2791 iteraciones, el número total de filas es de casi 38 millones.
  7. La unión de bucles anidados en el nodo 2 también aplica los predicados de unión. Comprueba que Operation_Typecoincide, que el Start_Timenodo 4 es menor que el Start_Timenodo 5, que el Start_Timenodo 5 es menor que el Finish_Timenodo 4 y que los dos Idvalores no coinciden.
  8. La recopilación de secuencias (detener el intercambio de paralelismo) en el nodo 1 combina las secuencias ordenadas de cada subproceso hasta que se hayan producido 100 filas. La naturaleza de preservación del orden de la fusión en varias secuencias es lo que requiere la fila adicional mencionada en el paso 5.

La gran ineficiencia es obviamente en los pasos 6 y 7 anteriores. Escanear completamente la tabla en el nodo 5 para cada iteración es incluso un poco razonable si solo sucede 94 veces como predice el optimizador. El conjunto de comparaciones de ~ 38 millones por fila en el nodo 2 también es un gran costo.

De manera crucial, es muy probable que la estimación del objetivo de la fila de la fila 93/94 sea incorrecta, ya que depende de la distribución de valores. El optimizador asume una distribución uniforme en ausencia de información más detallada. En términos simples, esto significa que si se espera que el 1% de las filas de la tabla califiquen, el optimizador razona que para encontrar 1 fila coincidente, debe leer 100 filas.

Si ejecutó esta consulta hasta su finalización (lo que puede llevar mucho tiempo), lo más probable es que tenga que leer más de 93/94 filas de la Clasificación para finalmente producir 100 filas. En el peor de los casos, la fila número 100 se encontraría utilizando la última fila del Ordenar. Suponiendo que la estimación del optimizador en el nodo 4 es correcta, esto significa ejecutar el Escaneo en el nodo 5 38,284 veces, para un total de algo así como 15 mil millones de filas. Podría ser más si las estimaciones de escaneo también están desactivadas.

Este plan de ejecución también incluye una advertencia de índice faltante:

/*
The Query Processor estimates that implementing the following index
could improve the query cost by 72.7096%.

WARNING: This is only an estimate, and the Query Processor is making
this recommendation based solely upon analysis of this specific query.
It has not considered the resulting index size, or its workload-wide
impact, including its impact on INSERT, UPDATE, DELETE performance.
These factors should be taken into account before creating this index.
*/

CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[Batch_Tasks_Queue] ([Operation_Type],[State],[Start_Time])
INCLUDE ([Id],[Parameters])

El optimizador lo alerta sobre el hecho de que agregar un índice a la tabla mejoraría el rendimiento.

Plan sin las columnas adicionales

Plan menos lento

Este es esencialmente el mismo plan que el anterior, con la adición de la cola de índice en el nodo 6 y el filtro en el nodo 5. Las diferencias importantes son:

  1. El Index Spool en el nodo 6 es un Eager Spool. Consume ansiosamente el resultado del análisis debajo de él, y crea un índice temporal con clave Operation_Typey Start_Time, Idcomo una columna sin clave.
  2. La unión de bucles anidados en el nodo 2 ahora es una unión de índice. No predicados de unión son evaluados aquí, en lugar de los valores de corriente por cada iteración de Operation_Type, Start_Time, Finish_Time, y Idde la exploración en el nodo 4 se pasan a la rama del lado interior como referencias externas.
  3. La exploración en el nodo 7 se realiza solo una vez.
  4. El Índice de carrete en el nodo 6 busca filas del índice temporal donde Operation_Typecoincide con el valor de referencia exterior actual, y el Start_Timeestá en el intervalo definido por los Start_Timey Finish_Timeexteriores referencias.
  5. El filtro en el nodo 5 prueba los Idvalores del carrete de índice para determinar la desigualdad con el valor de referencia externo actual de Id.

Las mejoras clave son:

  • El escaneo lateral interno se realiza solo una vez
  • Un índice temporal en ( Operation_Type, Start_Time) con Iduna columna incluida permite unir los bucles anidados de índice. El índice se utiliza para buscar filas coincidentes en cada iteración en lugar de escanear toda la tabla cada vez.

Como antes, el optimizador incluye una advertencia sobre un índice faltante:

/*
The Query Processor estimates that implementing the following index
could improve the query cost by 24.1475%.

WARNING: This is only an estimate, and the Query Processor is making
this recommendation based solely upon analysis of this specific query.
It has not considered the resulting index size, or its workload-wide
impact, including its impact on INSERT, UPDATE, DELETE performance.
These factors should be taken into account before creating this index.
*/

CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[Batch_Tasks_Queue] ([State],[Start_Time])
INCLUDE ([Id],[Operation_Type])
GO

Conclusión

El plan sin las columnas adicionales es más rápido porque el optimizador eligió crear un índice temporal para usted.

El plan con las columnas adicionales haría que el índice temporal sea más costoso de construir. La [Parameterscolumna] es nvarchar(2000), que agregaría hasta 4000 bytes a cada fila del índice. El costo adicional es suficiente para convencer al optimizador de que construir el índice temporal en cada ejecución no se pagaría por sí mismo.

El optimizador advierte en ambos casos que un índice permanente sería una mejor solución. La composición ideal del índice depende de su carga de trabajo más amplia. Para esta consulta en particular, los índices sugeridos son un punto de partida razonable, pero debe comprender los beneficios y costos involucrados.

Recomendación

Una amplia gama de posibles índices sería beneficiosa para esta consulta. La conclusión importante es que se necesita algún tipo de índice no agrupado. De la información proporcionada, un índice razonable en mi opinión sería:

CREATE NONCLUSTERED INDEX i1
ON dbo.Batch_Tasks_Queue (Start_Time DESC)
INCLUDE (Operation_Type, [State], Finish_Time);

También estaría tentado a organizar la consulta un poco mejor, y retrasaría la búsqueda de las [Parameters]columnas anchas en el índice agrupado hasta después de que se hayan encontrado las 100 filas superiores (utilizando Idcomo clave):

SELECT TOP (100)
    BTQ1.id,
    BTQ2.id,
    BTQ3.[Parameters],
    BTQ4.[Parameters]
FROM dbo.Batch_Tasks_Queue AS BTQ1
JOIN dbo.Batch_Tasks_Queue AS BTQ2 WITH (FORCESEEK)
    ON BTQ2.Operation_Type = BTQ1.Operation_Type
    AND BTQ2.Start_Time > BTQ1.Start_Time
    AND BTQ2.Start_Time < BTQ1.Finish_Time
    AND BTQ2.id != BTQ1.id
    -- Look up the [Parameters] values
JOIN dbo.Batch_Tasks_Queue AS BTQ3
    ON BTQ3.Id = BTQ1.Id
JOIN dbo.Batch_Tasks_Queue AS BTQ4
    ON BTQ4.Id = BTQ2.Id
WHERE
    BTQ1.[State] IN (3, 4)
    AND BTQ2.[State] IN (3, 4)
    AND BTQ1.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
    AND BTQ2.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
    -- These predicates are not strictly needed
    AND BTQ1.Start_Time IS NOT NULL
    AND BTQ2.Start_Time IS NOT NULL
ORDER BY
    BTQ1.Start_Time DESC;

Cuando las [Parameters]columnas no son necesarias, la consulta se puede simplificar para:

SELECT TOP (100)
    BTQ1.id,
    BTQ2.id
FROM dbo.Batch_Tasks_Queue AS BTQ1
JOIN dbo.Batch_Tasks_Queue AS BTQ2 WITH (FORCESEEK)
    ON BTQ2.Operation_Type = BTQ1.Operation_Type
    AND BTQ2.Start_Time > BTQ1.Start_Time
    AND BTQ2.Start_Time < BTQ1.Finish_Time
    AND BTQ2.id != BTQ1.id
WHERE
    BTQ1.[State] IN (3, 4)
    AND BTQ2.[State] IN (3, 4)
    AND BTQ1.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
    AND BTQ2.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
    AND BTQ1.Start_Time IS NOT NULL
    AND BTQ2.Start_Time IS NOT NULL
ORDER BY
    BTQ1.Start_Time DESC;

La FORCESEEKsugerencia está ahí para ayudar a garantizar que el optimizador elija un plan de bucles anidados indexados (existe una tentación basada en el costo para que el optimizador seleccione un hash o (muchos-muchos) fusionar, de lo contrario, lo que no suele funcionar bien con este tipo de consulta en la práctica. Ambos terminan con grandes residuos; muchos elementos por cubo en el caso del hash, y muchos rebobinados para la fusión).

Alternativa

Si la consulta (incluidos sus valores específicos) fuera particularmente crítica para el rendimiento de lectura, consideraría dos índices filtrados:

CREATE NONCLUSTERED INDEX i1
ON dbo.Batch_Tasks_Queue (Start_Time DESC)
INCLUDE (Operation_Type, [State], Finish_Time)
WHERE 
    Start_Time IS NOT NULL
    AND [State] IN (3, 4)
    AND Operation_Type <> 23
    AND Operation_Type <> 24
    AND Operation_Type <> 25
    AND Operation_Type <> 26
    AND Operation_Type <> 27
    AND Operation_Type <> 28
    AND Operation_Type <> 30;

CREATE NONCLUSTERED INDEX i2
ON dbo.Batch_Tasks_Queue (Operation_Type, [State], Start_Time)
WHERE 
    Start_Time IS NOT NULL
    AND [State] IN (3, 4)
    AND Operation_Type <> 23
    AND Operation_Type <> 24
    AND Operation_Type <> 25
    AND Operation_Type <> 26
    AND Operation_Type <> 27
    AND Operation_Type <> 28
    AND Operation_Type <> 30;

Para la consulta que no necesita la [Parameters]columna, el plan estimado que utiliza los índices filtrados es:

Plan de índice filtrado simple

La exploración de índice devuelve automáticamente todas las filas que califican sin evaluar ningún predicado adicional. Para cada iteración de la unión de bucles anidados de índice, la búsqueda de índice realiza dos operaciones de búsqueda:

  1. Una búsqueda de prefijo coincide con Operation_Typey State= 3, luego busca el rango de Start_Timevalores, predicado residual en la Iddesigualdad.
  2. Una búsqueda de prefijo coincide con Operation_Typey State= 4, luego busca el rango de Start_Timevalores, predicado residual de la Iddesigualdad.

Cuando [Parameters]se necesita la columna, el plan de consulta simplemente agrega un máximo de 100 búsquedas simples para cada tabla:

Plan de índice filtrado con columnas adicionales

Como nota final, debe considerar usar los tipos enteros estándar integrados en lugar de numericdonde corresponda.

Paul White 9
fuente
-2

Por favor cree el siguiente índice:

create index Batch_Tasks_Queue_ix_Start_Time on Batch_Tasks_Queue(Start_Time);
David דודו Markovitz
fuente
¿Lo has probado? He realizado algunas pruebas y marcó una gran diferencia.
David דודו Markovitz