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 1
Hinweis 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:
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:
Was ich bisher versuche
Mein erster Gedanke war, dass Hash Match
der 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 JOIN
sollte erzwingen, Nested Loop
sofort 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):
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):
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 =20825
den 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:
@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'
Der Parameter @p__linq__1
ist 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_counter
hö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.
quelle
Antworten:
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.
quelle
...OPTION (OPTIMIZE FOR UNKNOWN)
Hinweis behoben .