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 1
sugerencia 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í:
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:
Lo que intento hasta ahora
Mi primer pensamiento fue el Hash Match
chico malo. Así que modifiqué la consulta con una nueva pista.
...Extent1].[Date] = @p__linq__1) OPTION (MAXDOP 1, LOOP JOIN)
El LOOP JOIN
debería forzar a usar Nested Loop
instantá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ó):
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):
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 =20825
el 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:
@p__linq__0 = 1; @p__linq__1 = '2018-01-05 00:00:00.0000000'
@p__linq__0 = 2; @p__linq__1 = '2018-01-05 00:00:00.0000000'
@p__linq__0 = 3; @p__linq__1 = '2018-01-05 00:00:00.0000000'
El parámetro @p__linq__1
es 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_counter
es 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.