100% CPU mit schlechtem Ausführungsplan

8

Ich habe ein massives Problem mit 100% CPU-Spitzen aufgrund eines schlechten Ausführungsplans, der von einer bestimmten Abfrage verwendet wird. Ich verbringe jetzt Wochen damit, alleine zu lösen.

Meine Datenbank

Meine Beispieldatenbank enthält 3 vereinfachte Tabellen.

[Datenlogger]

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]

[Wandler]

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

Statistiken und Wartung

Die [InverterData]Tabelle enthält mehrere Millionen Zeilen (unterscheidet sich in mehreren Instanzen von PaaS), die in monatlichen Junks partitioniert sind.

Alle Indexer werden defragmentiert und alle Statistiken werden nach Bedarf in einer täglichen / wöchentlichen Runde neu erstellt / organisiert.

Meine Anfrage

Die Abfrage wird von Entity Framework generiert und ist auch einfach. Aber ich laufe 1.000 Mal pro Minute und Leistung ist wichtig.

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)

Der MAXDOP 1Hinweis ist für ein anderes Problem mit einem langsamen Parallelplan.

Der "gute" Plan

In 90% der Fälle ist der verwendete Plan blitzschnell und sieht folgendermaßen aus:

schneller Plan

Das Problem

Im Laufe des Tages änderte sich der gute Plan zufällig in einen schlechten und langsamen Plan.

Der "schlechte" Plan wird 10-60 Minuten lang verwendet und dann wieder in den "guten" Plan geändert. Der "schlechte" Plan erhöht die CPU auf 100%.

So sieht es aus:

langsamer Plan

Was ich bisher versuche

Mein erster Gedanke war, dass Hash Matchder böse Junge ist. Also habe ich die Abfrage mit einem neuen Hinweis geändert.

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

Das LOOP JOINsollte erzwingen, Nested Loopsofort zu verwenden Hash Match.

Das Ergebnis ist, dass der 90% -Plan wie zuvor aussieht. Aber der Plan änderte sich auch zufällig zu einem schlechten.

Der "schlechte" Plan sieht jetzt so aus (Reihenfolge der Tabellenschleifen geändert):

auch langsamer Plan

Die CPU späht auch während des "neuen schlechten" Plans zu 100%.

Lösung?

Mir fällt ein, den "guten" Plan zu erzwingen. Aber ich weiß nicht, ob das eine gute Idee ist.

Innerhalb des Plans wird ein Index empfohlen, der alle Spalten enthält. Dies verdoppelt jedoch den gesamten Tisch und verlangsamt die häufig vorkommenden Inserst.

Bitte hilf mir!


Update 1 - bezogen auf @ James Kommentar

Hier sind beide Pläne (einige zusätzliche Felder werden im Plan angezeigt, da sie aus der realen Tabelle stammen):

Guter Plan

Schlechter Plan 1 (Hash Match)

Schlechter Plan 2 (verschachtelte Schleife)

Update 2 - im Zusammenhang mit @David Fowler Antwort

Der schlechte Plan greift auf zufällige Parameterwerte zurück. Also normalerweise habe ich @p__linq__1 ='2016-11-26 00:00:00.0000000' @p__linq__0 =20825den ganzen Tag und dann den schlechten Plan auf den gleichen Wert gebracht.

Ich kenne das Problem des Parameter-Sniffing aus gespeicherten Prozeduren und weiß, wie man sie in SP vermeidet. Haben Sie einen Hinweis für mich, wie Sie dieses Problem bei meiner Abfrage vermeiden können?

Das Erstellen des empfohlenen Index enthält alle Spalten. Dies verdoppelt den gesamten Tisch und verlangsamt die häufig vorkommenden Inserst. Das "fühlt" sich nicht richtig an, einen Index zu erstellen, der einfach die Tabelle klont. Außerdem möchte ich die Datengröße dieser großen Tabelle verdoppeln.

Update 3 - bezogen auf @ David Fowler Kommentar

Es hat auch nicht funktioniert und ich denke, es konnte nicht. Zum besseren Verständnis werde ich Ihnen erklären, wie die Abfrage aufgerufen wird.

Nehmen wir an, ich habe 3 Entitäten in der [DataLogger]Tabelle. Im Laufe des Tages rufe ich immer wieder dieselben 3 Abfragen in einer Rundreise auf:

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

Parameter:

  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'

Der Parameter @p__linq__1ist immer das gleiche Datum. Bei einer Abfrage, die mehrere Male mit einem guten Plan ausgeführt wird, wird der schlechte Plan jedoch zufällig ausgewählt. Mit dem gleichen Parameter!

Update 4 - bezogen auf @Nic Kommentar

Die Wartung läuft jede Nacht und sieht so aus.

Index

Wenn ein Index mehr als 5% fragmentiert ist, wird er neu organisiert ...

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

Wenn ein Index zu mehr als 30% fragmentiert ist, wird er neu erstellt ...

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

Wenn der Index partitioniert ist, wird er auf Fragmentierung geprüft und pro Partition geändert ...

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

Statistiken

Alle Statistiken werden aktualisiert, wenn sie modification_counterhöher als 0 sind ...

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

oder auf partitioniert ..

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

Die Wartung umfasst alle Statistiken, auch die automatisch generierte.

Beispiel

Steffen Mangold
quelle
Sie erwähnen dies nicht, haben aber kürzlich die Statistiken für eine dieser Tabellen aktualisiert?
Nic
Thx @Nic Ich habe ein Update zu der Frage hinzugefügt. Detaillierte Informationen finden Sie dort.
Steffen Mangold

Antworten:

3

Schauen Sie sich die Pläne an, es gibt ein paar Unterschiede zwischen den guten und den schlechten. Als erstes ist zu beachten, dass der gute Plan eine Suche in InverterDayData durchführt, wobei beide schlechten Pläne einen Scan durchführen. Wenn Sie die geschätzten Zeilen überprüfen, werden Sie feststellen, dass der gute Plan 1 Zeile erwartet, während die schlechten Pläne 6661 und etwa 7000 Zeilen erwarten.

Schauen Sie sich nun die kompilierten Parameterwerte an.

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

Schlechte Pläne @ p__linq__1 = '2018-01-03 00: 00: 00.0000000' @ p__linq__0 = 20686

Für mich sieht es so aus, als wäre es ein Problem mit dem Parameter-Sniffing. Welche Parameterwerte übergeben Sie an diese Abfrage, wenn sie eine schlechte Leistung erbringt?

In den schlechten Plänen für InverterDayData gibt es eine Indexempfehlung, die vernünftig aussieht. Ich würde versuchen, diese auszuführen und zu prüfen, ob sie Ihnen hilft. Möglicherweise kann SQL einen Scan für die Tabelle durchführen.

David Fowler
quelle
danke für deine antwort! Ich habe die Frage für Sie (am Ende) geändert.
Steffen Mangold
Ich wusste nicht, dass der vorgeschlagene Index die gesamte Tabelle abdeckt, das ist keine gute Idee. Sie können einfach einen Index für die Datumsspalte versuchen.
David Fowler
Wenn Sie den Hinweis OPTIMIZE FOR verwenden, wird der Plan gezwungen, mit den guten Werten OPTION (OPTIMIZE FOR (@ p__linq__1 = '2016-11-26 00: 00: 00.0000000', @ p__linq__0 20825) zu kompilieren. Seien Sie jedoch vorsichtig mit diesem und testen Sie ihn um sicherzustellen, dass es keine unerwarteten Kopfschmerzen verursacht
David Fowler
hey @david Ich habe ein Update gepostet.
Steffen Mangold
Ok, es war ein Parameter-Sniffing-Problem! Aufgrund der unterschiedlichen Zeilenanzahl in der partitionierten Tabelle wird Index SEEK oder SCAN ausgewählt. Ich habe es mit einem ...OPTION (OPTIMIZE FOR UNKNOWN)Hinweis behoben .
Steffen Mangold