Eines der verwirrenderen Probleme, die in SQL Server behoben werden müssen, können Probleme im Zusammenhang mit Arbeitsspeicherzuweisungen sein. Einige Abfragen benötigen zur Ausführung mehr Arbeitsspeicher als andere, je nachdem, welche Operationen ausgeführt werden müssen (z. B. Sortieren, Hash). Der Optimierer von SQL Server schätzt, wie viel Arbeitsspeicher benötigt wird, und die Abfrage muss die Arbeitsspeicherzuteilung erhalten, um mit der Ausführung zu beginnen. Es behält diese Berechtigung für die Dauer der Abfrageausführung bei – was bedeutet, dass Sie auf Parallelitätsprobleme stoßen können, wenn der Optimierer den Arbeitsspeicher überschätzt. Wenn der Speicher unterschätzt wird, können Sie Überläufe in tempdb sehen. Beides ist nicht ideal, und wenn Sie einfach zu viele Abfragen haben, die mehr Speicher anfordern, als zur Verfügung steht, sehen Sie, dass RESOURCE_SEMAPHORE wartet. Es gibt mehrere Möglichkeiten, dieses Problem anzugehen, und eine meiner neuen Lieblingsmethoden ist die Verwendung von Query Store.
Einrichtung
Wir verwenden eine Kopie von WideWorldImporters, die ich mithilfe der gespeicherten Prozedur DataLoadSimulation.DailyProcessToCreateHistory aufgeblasen habe. Die Tabelle „Sales.Orders“ hat etwa 4,6 Millionen Zeilen und die Tabelle „Sales.OrderLines“ etwa 9,2 Millionen Zeilen. Wir werden die Sicherung wiederherstellen und den Abfragespeicher aktivieren und alle alten Abfragespeicherdaten löschen, damit wir keine Metriken für diese Demo ändern.
Erinnerung:ALTER DATABASE
Die gespeicherte Prozedur, die wir zum Testen verwenden, fragt die oben genannten Tabellen „Orders“ und „OrderLines“ basierend auf einem Datumsbereich ab:
USE [WideWorldImporters]; GO DROP PROCEDURE IF EXISTED [Sales].[usp_OrderInfo_OrderDate]; GO CREATE PROCEDURE [Sales].[usp_OrderInfo_OrderDate] @StartDate DATETIME, @EndDate DATETIME AS SELECT [o].[CustomerID], [o].[OrderDate], [o].[ContactPersonID], [ol].[Menge] FROM [Sales].[Orders] [o] JOIN [Sales].[OrderLines] [ol] ON [o].[OrderID] =[ol].[OrderID] WHERE [OrderDate] BETWEEN @StartDate AND @EndDate ORDER BY [Bestelldatum]; GEHEN
Testen
Wir werden die gespeicherte Prozedur mit drei verschiedenen Sätzen von Eingabeparametern ausführen:
EXEC [Verkäufe].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-01-08'; GO EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-06-30'; GO EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-12-31'; GEHEN
Die erste Ausführung gibt 1958 Zeilen zurück, die zweite 267.268 Zeilen und die letzte über 2,2 Millionen Zeilen. Wenn Sie sich die Datumsbereiche ansehen, ist dies nicht überraschend – je größer der Datumsbereich, desto mehr Daten werden zurückgegeben.
Da es sich um eine gespeicherte Prozedur handelt, bestimmen die verwendeten Eingabeparameter zunächst den Plan sowie den zu gewährenden Speicher. Wenn wir uns den tatsächlichen Ausführungsplan für die erste Ausführung ansehen, sehen wir verschachtelte Schleifen und eine Speicherzuteilung von 2656 KB.
Nachfolgende Ausführungen haben denselben Plan (da dieser zwischengespeichert wurde) und dieselbe Speicherzuweisung, aber wir bekommen einen Hinweis darauf, dass dies nicht ausreicht, weil es eine Sortierwarnung gibt.
Wenn wir im Abfragespeicher nach dieser gespeicherten Prozedur suchen, sehen wir drei Ausführungen und die gleichen Werte für UsedKB-Speicher, unabhängig davon, ob wir Durchschnitt, Minimum, Maximum, Letzte oder Standardabweichung betrachten. Hinweis:Informationen zur Arbeitsspeicherzuteilung im Abfragespeicher werden als Anzahl von 8-KB-Seiten gemeldet.
SELECT [qst].[query_sql_text], [qsq].[query_id], [qsp].[plan_id], [qsq].[object_id], [rs].[count_executions], [rs].[last_execution_time ], [rs].[avg_duration], [rs].[avg_logical_io_reads], [rs].[avg_query_max_used_memory] * 8 AS [AvgUsedKB], [rs].[min_query_max_used_memory] * 8 AS [MinUsedKB], --memory grant (angegeben als Anzahl von 8-KB-Seiten) für den Abfrageplan innerhalb des Aggregationsintervalls [rs].[max_query_max_used_memory] * 8 AS [MaxUsedKB], [rs].[last_query_max_used_memory] * 8 AS [LastUsedKB], [rs]. [stdev_query_max_used_memory] * 8 AS [StDevUsedKB], TRY_CONVERT(XML, [qsp].[query_plan]) AS [QueryPlan_XML] FROM [sys].[query_store_query] [qsq] JOIN [sys].[query_store_query_text] [qst] ON [ qsq].[query_text_id] =[qst].[query_text_id] JOIN [sys].[query_store_plan] [qsp] ON [qsq].[query_id] =[qsp].[query_id] JOIN [sys].[query_store_runtime_stats] [ rs] ON [qsp].[plan_id] =[rs].[plan_id] WHERE [qsq].[object_id] =OBJECT_ID(N'Sale s.usp_OrderInfo_OrderDate');
Wenn wir in diesem Szenario – wo ein Plan zwischengespeichert und wiederverwendet wird – nach Problemen mit der Speicherzuweisung suchen, hilft uns Query Store nicht weiter.
Was aber, wenn die spezifische Abfrage bei der Ausführung kompiliert wird, entweder aufgrund eines RECOMPILE-Hinweises oder weil es sich um eine Ad-hoc-Anfrage handelt?
Wir können die Prozedur ändern, um den RECOMPILE-Hinweis zur Anweisung hinzuzufügen (was gegenüber dem Hinzufügen von RECOMPILE auf Prozedurebene oder dem Ausführen der Prozedur WITH RECOMIPLE empfohlen wird):
ALTER PROCEDURE [Sales].[usp_OrderInfo_OrderDate] @StartDate DATETIME, @EndDate DATETIME AS SELECT [o].[CustomerID], [o].[OrderDate], [o].[ContactPersonID], [ol].[ Menge] FROM [Sales].[Orders] [o] JOIN [Sales].[OrderLines] [ol] ON [o].[OrderID] =[ol].[OrderID] WHERE [OrderDate] BETWEEN @StartDate AND @EndDate BESTELLEN NACH [Bestelldatum] OPTION (NEU KOMPILIEREN); GEHEN
Jetzt führen wir unsere Prozedur mit denselben Eingabeparametern wie zuvor erneut aus und überprüfen die Ausgabe:
Beachten Sie, dass wir eine neue query_id haben – der Abfragetext hat sich geändert, weil wir OPTION (RECOMPILE) hinzugefügt haben – und wir haben auch zwei neue plan_id-Werte und wir haben unterschiedliche Speicherzuweisungsnummern für einen unserer Pläne. Für plan_id 5 gibt es nur eine Ausführung, und die Speicherzuteilungsnummern stimmen mit der anfänglichen Ausführung überein – dieser Plan gilt also für den kleinen Datumsbereich. Die beiden größeren Datumsbereiche generierten denselben Plan, aber es gibt erhebliche Unterschiede bei den Speicherzuteilungen – 94.528 für das Minimum und 573.568 für das Maximum.
Wenn wir uns die Informationen zur Speicherzuteilung mit den Query Store-Berichten ansehen, zeigt sich diese Variabilität etwas anders. Wenn wir den Bericht „Top-Ressourcenverbraucher“ aus der Datenbank öffnen und dann die Metrik in „Speicherverbrauch (KB)“ und „Durchschnitt“ ändern, kommt unsere Abfrage mit „RECOMPILE“ an den Anfang der Liste.
In diesem Fenster werden Metriken nach Abfrage aggregiert, nicht nach Plan. Die Abfrage, die wir direkt für die Ansichten des Abfragespeichers ausgeführt haben, listete nicht nur die query_id, sondern auch die plan_id auf. Hier können wir sehen, dass die Abfrage zwei Pläne hat, und wir können sie beide im Planzusammenfassungsfenster anzeigen, aber die Metriken werden für alle Pläne in dieser Ansicht kombiniert.
Die Variabilität der Speicherzuteilungen ist offensichtlich, wenn wir uns die Ansichten direkt ansehen. Wir können Abfragen mit Variabilität mithilfe der Benutzeroberfläche finden, indem wir die Statistik von Avg in StDev ändern:
Wir können die gleichen Informationen finden, indem wir die Ansichten des Abfragespeichers abfragen und nach stdev_query_max_used_memory absteigend sortieren. Wir können aber auch basierend auf der Differenz zwischen der minimalen und maximalen Speicherzuteilung oder einem Prozentsatz der Differenz suchen. Wenn wir uns beispielsweise Sorgen über Fälle machen, in denen der Unterschied in den Zuschüssen größer als 512 MB ist, könnten wir Folgendes ausführen:
SELECT [qst].[query_sql_text], [qsq].[query_id], [qsp].[plan_id], [qsq].[object_id], [rs].[count_executions], [rs].[last_execution_time ], [rs].[avg_duration], [rs].[avg_logical_io_reads], [rs].[avg_query_max_used_memory] * 8 AS [AvgUsedKB], [rs].[min_query_max_used_memory] * 8 AS [MinUsedKB], [rs]. [max_query_max_used_memory] * 8 AS [MaxUsedKB], [rs].[last_query_max_used_memory] * 8 AS [LastUsedKB], [rs].[stdev_query_max_used_memory] * 8 AS [StDevUsedKB], TRY_CONVERT(XML, [qsp].[query_plan]) AS [QueryPlan_XML] FROM [sys].[query_store_query] [qsq] JOIN [sys].[query_store_query_text] [qst] ON [qsq].[query_text_id] =[qst].[query_text_id] JOIN [sys].[query_store_plan] [qsp] ON [qsq].[query_id] =[qsp].[query_id] JOIN [sys].[query_store_runtime_stats] [rs] ON [qsp].[plan_id] =[rs].[plan_id] WHERE ([rs ].[max_query_max_used_memory]*8) - ([rs].[min_query_max_used_memory]*8)> 524288;
Diejenigen unter Ihnen, die SQL Server 2017 mit Columnstore-Indizes ausführen und den Vorteil des Memory Grant-Feedbacks haben, können diese Informationen auch im Abfragespeicher verwenden. Zuerst ändern wir unsere Orders-Tabelle, um einen gruppierten Columnstore-Index hinzuzufügen:
ALTER TABLE [Umsätze].[Rechnungen] DROP CONSTRAINT [FK_Sales_Invoices_OrderID_Sales_Orders]; GO ALTER TABLE [Verkäufe].[Bestellungen] DROP CONSTRAINT [FK_Sales_Orders_BackorderOrderID_Sales_Orders]; GO ALTER TABLE [Sales].[OrderLines] DROP CONSTRAINT [FK_Sales_OrderLines_OrderID_Sales_Orders]; GO ALTER TABLE [Sales].[Orders] DROP CONSTRAINT [PK_Sales_Orders] WITH ( ONLINE =OFF ); GO CREATE CLUSTERED COLUMNSTORE INDEX CCI_Orders ON [Sales].[Orders];
Dann setzen wir den Datenbank-Kombinierbarkeitsmodus auf 140, damit wir das Feedback zur Speicherzuteilung nutzen können:
ALTER DATABASE [WideWorldImporters] SET COMPATIBILITY_LEVEL =140; GEHEN
Schließlich ändern wir unsere gespeicherte Prozedur, um OPTION (RECOMPILE) aus unserer Abfrage zu entfernen, und führen sie dann einige Male mit den verschiedenen Eingabewerten aus:
ALTER PROCEDURE [Sales].[usp_OrderInfo_OrderDate] @StartDate DATETIME, @EndDate DATETIME AS SELECT [o].[CustomerID], [o].[OrderDate], [o].[ContactPersonID], [ol].[ Menge] FROM [Sales].[Orders] [o] JOIN [Sales].[OrderLines] [ol] ON [o].[OrderID] =[ol].[OrderID] WHERE [OrderDate] BETWEEN @StartDate AND @EndDate BESTELLUNG BIS [Bestelldatum]; GO EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-01-08'; GO EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-06-30'; GO EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-12-31'; GO EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-06-30'; GO EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-01-08'; GO EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-12-31'; GEHEN
Innerhalb des Abfragespeichers sehen wir Folgendes:
Wir haben einen neuen Plan für query_id =1, der andere Werte für die Speicherzuweisungsmetriken und einen etwas niedrigeren StDev als bei plan_id 6 hat. Wenn wir uns den Plan im Abfragespeicher ansehen, sehen wir, dass er auf den gruppierten Columnstore-Index zugreift :
Denken Sie daran, dass der Plan im Abfragespeicher ausgeführt wurde, aber nur Schätzungen enthält. Während für den Plan im Plan-Cache Speicherzuweisungsinformationen aktualisiert werden, wenn Speicherrückmeldungen auftreten, werden diese Informationen nicht auf den vorhandenen Plan im Abfragespeicher angewendet.
Zusammenfassung
Folgendes gefällt mir an der Verwendung von Query Store zum Betrachten von Abfragen mit variablen Speicherzuweisungen:Die Daten werden automatisch erfasst. Wenn dieses Problem unerwartet auftritt, müssen wir nichts unternehmen, um Informationen zu sammeln, wir haben sie bereits im Query Store erfasst. In dem Fall, in dem eine Abfrage parametrisiert ist, kann es aufgrund des Potenzials für statische Werte aufgrund des Plan-Cachings schwieriger sein, die Variabilität der Speicherzuweisung zu finden. Wir können jedoch auch feststellen, dass die Abfrage aufgrund der Neukompilierung mehrere Pläne mit extrem unterschiedlichen Speicherzuweisungswerten hat, die wir verwenden könnten, um das Problem aufzuspüren. Es gibt verschiedene Möglichkeiten, das Problem mithilfe der im Abfragespeicher erfassten Daten zu untersuchen, und Sie können Probleme sowohl proaktiv als auch reaktiv betrachten.