100% CPU con mal plan de ejecución

8

Tengo un problema masivo con picos de CPU del 100% debido a un mal plan de ejecución utilizado por una consulta específica. Ahora paso semanas resolviendo por mi cuenta.

Mi base de datos

Mi base de datos de muestra contiene 3 tablas simplificadas.

[Registrador de datos]

CREATE TABLE [model].[DataLogger](
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [ProjectID] [bigint] NULL,
CONSTRAINT [PK_DataLogger] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]

[Inversor]

CREATE TABLE [model].[Inverter](
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [SerialNumber] [nvarchar](50) NOT NULL,
 CONSTRAINT [PK_Inverter] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY],
 CONSTRAINT [UK_Inverter] UNIQUE NONCLUSTERED 
(
    [DataLoggerID] ASC,
    [SerialNumber] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]

ALTER TABLE [model].[Inverter] WITH CHECK
ADD CONSTRAINT [FK_Inverter_DataLogger]
FOREIGN KEY([DataLoggerID])
REFERENCES [model].[DataLogger] ([ID])

[InverterData]

CREATE TABLE [data].[InverterData](
    [InverterID] [bigint] NOT NULL,
    [Timestamp] [datetime] NOT NULL,
    [DayYield] [decimal](18, 2) NULL,
 CONSTRAINT [PK_InverterData] PRIMARY KEY CLUSTERED 
(
    [InverterID] ASC,
    [Timestamp] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF)
)

Estadísticas y mantenimiento

La [InverterData]tabla contiene varios millones de filas (difiere en varias instancias de PaaS) particionadas en basura mensual.

Todos los indexadores se desfragmentan y todas las estadísticas se reconstruyen / reorganizan según sea necesario en un turno diario / semanal.

Mi consulta

La consulta se genera en Entity Framework y también es simple. Pero corro 1,000 veces por minuto y el rendimiento es esencial.

SELECT
[Extent1].[InverterID] AS [InverterID],
[Extent1].[DayYield] AS [DayYield]
FROM [data].[InverterDayData] AS [Extent1]
INNER JOIN [model].[Inverter] AS [Extent2] ON [Extent1].[InverterID] = [Extent2].[ID]
INNER JOIN [model].[DataLogger] AS [Extent3] ON [Extent2].[DataLoggerID] = [Extent3].[ID]
WHERE ([Extent3].[ProjectID] = @p__linq__0)
AND ([Extent1].[Date] = @p__linq__1) OPTION (MAXDOP 1)

La MAXDOP 1sugerencia es para otro problema con un plan paralelo lento.

El "buen" plan

Durante el 90% del tiempo, el plan utilizado es extremadamente rápido y se ve así:

plan rápido

El problema

Durante el día, el buen plan cambió aleatoriamente a un plan malo y lento.

El plan "malo" se usa durante 10-60 minutos y luego se vuelve a cambiar al plan "bueno". El plan "malo" eleva la CPU hasta el 100% permanente.

Así es como se ve:

plan lento

Lo que intento hasta ahora

Mi primer pensamiento fue el Hash Matchchico malo. Así que modifiqué la consulta con una nueva pista.

...Extent1].[Date] = @p__linq__1) OPTION (MAXDOP 1, LOOP JOIN)

El LOOP JOINdebería forzar a usar Nested Loopinstantáneo de Hash Match.

El resultado es que el plan del 90% se ve como antes. Pero el plan también cambió aleatoriamente a uno malo.

El plan "malo" ahora se ve así (el orden del bucle de la tabla cambió):

también plan lento

La CPU también alcanza el 100% durante el "nuevo plan malo".

¿Solución?

Se me ocurre forzar el "buen" plan. Pero no sé si es una buena idea.

Dentro del plan hay un índice recomendado que incluye todas las columnas. Pero esto duplicará la tabla completa y ralentizará la inserción que son muy frecuentes.

¡Por favor, ayúdame!


Actualización 1 - relacionada con el comentario de @James

Aquí hay dos planes (algunos campos adicionales se muestran en el plan porque es de la tabla real):

Buen plan

Mal plan 1 (Hash Match)

Mal plan 2 (bucle anidado)

Actualización 2 - relacionada con @David Fowler answere

El mal plan está dando inicio a un valor de parámetro aleatorio. Entonces, normalmente, @p__linq__1 ='2016-11-26 00:00:00.0000000' @p__linq__0 =20825el día del hoyo y el mal plan tienen el mismo valor.

Sé el problema de detección de parámetros de los procedimientos almacenados y cómo evitarlos dentro del SP. ¿Tiene alguna pista para mí cómo evitar este problema en mi consulta?

La creación del índice recomendado incluirá todas las columnas. Esto duplicará la tabla completa y ralentizará la inserción, que son muy frecuentes. Eso no "se siente" bien para construir un índice que simplemente clone la tabla. También quiero decir duplicar el tamaño de los datos de esta gran tabla.

Actualización 3 - relacionada con el comentario de @David Fowler

Tampoco funcionó y creo que no pudo. Para una mejor comprensión, le explicaré cómo se llama la consulta.

Supongamos que tengo 3 entidades en la [DataLogger]tabla. Durante el día llamo las mismas 3 consultas en un viaje de ida y vuelta una y otra vez:

Consulta base: ...WHERE ([Extent3].[ProjectID] = @p__linq__0) AND ([Extent1].[Date] = @p__linq__1)

Parámetro:

  1. @p__linq__0 = 1; @p__linq__1 = '2018-01-05 00:00:00.0000000'
  2. @p__linq__0 = 2; @p__linq__1 = '2018-01-05 00:00:00.0000000'
  3. @p__linq__0 = 3; @p__linq__1 = '2018-01-05 00:00:00.0000000'

El parámetro @p__linq__1es siempre la misma fecha. Pero elige el mal plan al azar en una consulta que se ejecuta muchas veces con un buen plan antes. Con el mismo parámetro!

Actualización 4 - relacionada con el comentario de @Nic

El mantenimiento se ejecuta todas las noches y se ve así.

Índice

Si un índice se fragmenta más del 5%, se reorganiza ...

ALTER INDEX [{index}] ON [{table}] REORGANIZE

Si un índice se fragmenta más del 30%, se reconstruye ...

ALTER INDEX [{index}] ON [{table}] REBUILD WITH (ONLINE=ON, MAXDOP=1)

Si el índice está particionado, se comprobará la fragmentación y se modificará por partición ...

ALTER INDEX [{index}] ON [{table}] REBUILD PARTITION = {partitionNr} WITH (ONLINE=ON, MAXDOP=1)

Estadísticas

Todas las estadísticas se actualizarán si modification_counteres superior a 0 ...

UPDATE STATISTICS [{schema}].[{object}] ([{stats}]) WITH FULLSCAN

o en particionado ..

UPDATE STATISTICS [{schema}].[{object}] ([{stats}]) WITH RESAMPLE ON PARTITIONS({partitionNr})

El mantenimiento incluye todas las estadísticas, también la generada automáticamente.

Ejemplo

Steffen Mangold
fuente
No menciona esto, pero ¿ha actualizado las estadísticas en alguna de estas tablas recientemente?
Nic
Thx @Nic Agregué una actualización sobre la pregunta. Por favor, vea la información detallada allí.
Steffen Mangold

Respuestas:

3

Mire los planes, hay algunas diferencias entre el bueno y el malo. Lo primero que debe notar es que el buen plan realiza una búsqueda en InverterDayData donde los dos malos planes realizan un escaneo. ¿Por qué es esto? Si verifica las filas estimadas, verá que el buen plan espera 1 fila, mientras que los planes malos esperan 6661 y alrededor de 7000 filas.

Ahora eche un vistazo a los valores de parámetros compilados,

Buen plan @ p__linq__1 = '2016-11-26 00: 00: 00.0000000' @ p__linq__0 = 20825

Malos planes @ p__linq__1 = '2018-01-03 00: 00: 00.0000000' @ p__linq__0 = 20686

así que me parece que es un problema de detección de parámetros, ¿qué valores de parámetros está pasando a esa consulta cuando funciona mal?

Hay una recomendación de índice en los malos planes en InverterDayData que parece razonable, trataría de ejecutar eso y ver si te ayuda. Puede permitir que SQL realice una exploración en la tabla.

David Fowler
fuente
gracias por tu respuesta! He modificado la pregunta para ti (al final).
Steffen Mangold
No me di cuenta de que el índice sugerido cubría toda la tabla, esa no es una buena idea. Podrías probar un índice en la columna de fecha.
David Fowler
el uso de la sugerencia OPTIMIZE FOR obligará al plan a compilar con los buenos valores, OPTION (OPTIMIZE FOR (@ p__linq__1 = '2016-11-26 00: 00: 00.0000000', @ p__linq__0 20825)) pero tenga cuidado con ese y pruebe para asegurarte de que no te va a causar ningún dolor de cabeza inesperado
David Fowler
hola @david publiqué una actualización.
Steffen Mangold
Ok, fue un problema de detección de parámetros. Debido al diferente recuento de filas dentro de la tabla particionada, se elige Index SEEK o SCAN. Lo arreglé con una ...OPTION (OPTIMIZE FOR UNKNOWN)pista.
Steffen Mangold