CPU al 100% con piano di esecuzione errato


8

Ho un grosso problema con picchi di CPU al 100% a causa di un piano di esecuzione errato utilizzato da una query specifica. Passo settimane a risolverlo da solo.

Il mio database

Il mio DB di esempio contiene 3 tabelle semplificate.

[Datalogger]

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]

[Inverter]

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)
)

Statistica e manutenzione

La [InverterData]tabella contiene più milioni di righe (differisce in più istanze PaaS) partizionate in giunche mensili.

Tutti gli indicizzatori vengono deframmentati e tutte le statistiche vengono ricostruite / riorganizzate secondo necessità in un turno giornaliero / settimanale.

La mia domanda

La query è generata da Entity Framework e anche semplice. Ma corro 1.000 volte al minuto e le prestazioni sono essenziali.

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)

Il MAXDOP 1suggerimento è per un altro problema con un piano parallelo parallelo.

Il "buon" piano

Nel 90% dei casi il piano utilizzato è velocissimo e si presenta così:

piano veloce

Il problema

Nel corso della giornata il buon piano è cambiato casualmente in un piano cattivo e lento.

Il piano "cattivo" viene utilizzato per 10-60 minuti e quindi ripristinato al piano "buono". Il piano "cattivo" porta la CPU al 100% permanente.

Ecco come appare:

piano lento

Quello che provo finora

Il mio primo pensiero è stato Hash Matchil cattivo ragazzo. Quindi ho modificato la query con un nuovo suggerimento.

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

L' LOOP JOINdovrebbe costringere a usare Nested Loopistante di Hash Match.

Il risultato è che il piano del 90% appare come prima. Ma il piano è cambiato anche casualmente in uno cattivo.

Il piano "cattivo" ora assomiglia a questo (ordine del loop di tabella modificato):

piano anche lento

La CPU fa anche capolino al 100% durante il piano "nuovo cattivo".

Soluzione?

Mi viene in mente di forzare il "buon" piano. Ma non so se questa è una buona idea.

All'interno del piano è consigliato un indice che includesse tutte le colonne. Ma questo raddoppierà il tavolo completo e rallenterà gli inserst che sono molto frequenti.

Mi aiuti per favore!


Aggiornamento 1 - correlato al commento @James

Ecco entrambi i piani (alcuni campi extra mostrati nel piano perché provengono dalla tabella reale):

Buon piano

Bad plan 1 (Hash Match)

Bad plan 2 (Nested Loop)

Aggiornamento 2 - relativo alla risposta di @David Fowler

Il cattivo piano sta dando il via al valore di parametro casuale. Così normalmente @p__linq__1 ='2016-11-26 00:00:00.0000000' @p__linq__0 =20825il giorno del buco e il piano cattivo che arriva sullo stesso valore.

Conosco il problema dello sniffing dei parametri dalle procedure memorizzate e come evitarli all'interno di SP. Hai un suggerimento per me come evitare questo problema per la mia query?

La creazione dell'indice raccomandato includerà tutte le colonne. Questo raddoppierà la tabella completa e rallenterà gli inserst, che sono molto frequenti. Ciò non "sembra" giusto per costruire un indice che cloni semplicemente la tabella. Inoltre intendo raddoppiare la dimensione dei dati di questa grande tabella.

Aggiornamento 3 - correlato al commento di @David Fowler

Inoltre non ha funzionato e penso che non potrebbe. Per una migliore comprensione ti spiegherò come viene chiamata la query.

Supponiamo che io abbia 3 entità nella [DataLogger]tabella. Nel corso della giornata chiamo ripetutamente le stesse 3 domande in un viaggio di andata e ritorno:

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

Parametro:

  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'

Il parametro @p__linq__1è sempre la stessa data. Ma sceglie il piano errato in modo casuale su una query che viene eseguita migliaia di volte con un buon piano prima. Con lo stesso parametro!

Aggiornamento 4 - relativo al commento @Nic

La manutenzione viene eseguita ogni notte e si presenta così.

Indice

Se un indice viene frammentato più del 5%, viene riorganizzato ...

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

Se un indice viene frammentato più del 30%, viene ricostruito ...

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

Se l'Indice è partizionato verrà sottoposto a prova di frammentazione e modificato per partizione ...

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

statistica

Tutte le statistiche verranno aggiornate se modification_counterè superiore a 0 ...

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

o su partizionato ..

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

La manutenzione include tutte le statistiche, anche quella generata automaticamente.

Esempio


Non lo dici, ma hai aggiornato le statistiche su una di queste tabelle di recente?
Nic

Thx @Nic Ho aggiunto un aggiornamento alla domanda. Si prega di consultare informazioni dettagliate lì.
Steffen Mangold,

Risposte:


3

Guarda i piani, ci sono alcune differenze tra il buono e il cattivo. La prima cosa da notare è che il piano buono esegue una ricerca su InverterDayData in cui entrambi i piani cattivi eseguono una scansione. Perché, se controlli le righe stimate, vedrai che il piano buono prevede 1 riga, mentre i piani cattivi prevedono 6661 e circa 7000 righe.

Ora dai un'occhiata ai valori dei parametri compilati,

Good Plan @ p__linq__1 = '2016-11-26 00: 00: 00.0000000' @ p__linq__0 = 20825

Piani errati @ p__linq__1 = '2018-01-03 00: 00: 00.0000000' @ p__linq__0 = 20686

quindi mi sembra che sia un problema di sniffing dei parametri, quali valori dei parametri stai passando a quella query quando sta funzionando male?

C'è una raccomandazione sull'indice nei cattivi piani su InverterDayData che sembra ragionevole, proverei a eseguirla e vedere se ti aiuta. Potrebbe consentire a SQL di eseguire una scansione sulla tabella.


grazie per la tua risposta! Ho modificato la domanda per te (alla fine).
Steffen Mangold,

Non mi ero reso conto che l'indice suggerito coprisse l'intero tavolo, non è una buona idea. Potresti semplicemente provare un indice nella colonna della data.
David Fowler,

l'utilizzo del suggerimento OPTIMIZE FOR costringerà il piano a compilare con i buoni valori, OPTION (OPTIMIZE FOR (@ p__linq__1 = '2016-11-26 00: 00: 00.0000000', @ p__linq__0 20825)) ma fai attenzione con quello e testa per assicurarti che non ti causi mal di testa inaspettati
David Fowler,

hey @david ho pubblicato un aggiornamento.
Steffen Mangold,

Ok, è stato un problema di sniffing dei parametri! A causa del diverso conteggio delle righe all'interno della tabella partizionata, viene scelto Index SEEK o SCAN. L'ho risolto con un ...OPTION (OPTIMIZE FOR UNKNOWN)suggerimento.
Steffen Mangold,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.