SQL Serverでトラブルシューティングする必要のあるより厄介な問題の1つは、メモリの許可に関連する問題である可能性があります。一部のクエリは、実行する必要のある操作(ソート、ハッシュなど)に基づいて、実行するために他のクエリよりも多くのメモリを必要とします。 SQL Serverのオプティマイザは、必要なメモリの量を見積もります。クエリは、実行を開始するためにメモリ許可を取得する必要があります。クエリ実行中はその許可が保持されます。つまり、オプティマイザがメモリを過大評価すると、同時実行の問題が発生する可能性があります。メモリを過小評価している場合は、tempdbに流出が見られます。どちらも理想的ではありません。許可できるメモリよりも多くのメモリを要求するクエリが多すぎる場合は、RESOURCE_SEMAPHOREが待機していることがわかります。この問題を攻撃する方法は複数ありますが、私の新しいお気に入りの方法の1つは、クエリストアを使用することです。
セットアップ
DataLoadSimulation.DailyProcessToCreateHistoryストアドプロシージャを使用してインフレートしたWideWorldImportersのコピーを使用します。 Sales.Ordersテーブルには約460万行、Sales.OrderLinesテーブルには約920万行があります。バックアップを復元してクエリストアを有効にし、古いクエリストアデータをクリアして、このデモの指標を変更しないようにします。
注意:ALTER DATABASE
USE [master]; GO RESTORE DATABASE [WideWorldImporters] FROM DISK = N'C:\Backups\WideWorldImporters.bak' WITH FILE = 1, MOVE N'WWI_Primary' TO N'C:\Databases\WideWorldImporters\WideWorldImporters.mdf', MOVE N'WWI_UserData' TO N'C:\Databases\WideWorldImporters\WideWorldImporters_UserData.ndf', MOVE N'WWI_Log' TO N'C:\Databases\WideWorldImporters\WideWorldImporters.ldf', NOUNLOAD, REPLACE, STATS = 5 GO ALTER DATABASE [WideWorldImporters] SET QUERY_STORE = ON; GO ALTER DATABASE [WideWorldImporters] SET QUERY_STORE ( OPERATION_MODE = READ_WRITE, INTERVAL_LENGTH_MINUTES = 10 ); GO ALTER DATABASE [WideWorldImporters] SET QUERY_STORE CLEAR; GO
テストに使用するストアドプロシージャは、日付範囲に基づいて前述のOrdersテーブルとOrderLinesテーブルをクエリします。
USE [WideWorldImporters]; GO DROP PROCEDURE IF EXISTS [Sales].[usp_OrderInfo_OrderDate]; GO CREATE PROCEDURE [Sales].[usp_OrderInfo_OrderDate] @StartDate DATETIME, @EndDate DATETIME AS SELECT [o].[CustomerID], [o].[OrderDate], [o].[ContactPersonID], [ol].[Quantity] FROM [Sales].[Orders] [o] JOIN [Sales].[OrderLines] [ol] ON [o].[OrderID] = [ol].[OrderID] WHERE [OrderDate] BETWEEN @StartDate AND @EndDate ORDER BY [OrderDate]; GO
テスト
ストアドプロシージャを実行して、3つの異なる入力パラメータセットを実行します。
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
最初の実行は1958行を返し、2番目の実行は267,268行を返し、最後の実行は220万行を超えます。日付範囲を見ると、これは驚くべきことではありません。日付範囲が大きいほど、返されるデータが多くなります。
これはストアドプロシージャであるため、最初に使用される入力パラメータによって、計画と付与されるメモリが決まります。最初の実行の実際の実行プランを見ると、ネストされたループと2656KBのメモリ許可があります。
後続の実行には同じ計画(キャッシュされたものと同じ)と同じメモリ許可がありますが、ソート警告があるため、それだけでは不十分であるという手がかりが得られます。
クエリストアでこのストアドプロシージャを確認すると、平均、最小、最大、最後、または標準偏差のいずれを確認しても、UsedKBメモリに対して3つの実行と同じ値が表示されます。 注:クエリストアのメモリ付与情報は、8KBページの数として報告されます。
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 (reported as the number of 8 KB pages) for the query plan within the aggregation interval [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'Sales.usp_OrderInfo_OrderDate');
プランがキャッシュされて再利用されるこのシナリオでメモリ付与の問題を探している場合、クエリストアは役に立ちません。
しかし、RECOMPILEヒントのため、またはアドホックのために、特定のクエリが実行時にコンパイルされた場合はどうなりますか?
プロシージャを変更して、ステートメントにRECOMPILEヒントを追加できます(プロシージャレベルでRECOMPILEを追加したり、プロシージャWITH RECOMIPLEを実行したりするよりも推奨されます):
ALTER PROCEDURE [Sales].[usp_OrderInfo_OrderDate] @StartDate DATETIME, @EndDate DATETIME AS SELECT [o].[CustomerID], [o].[OrderDate], [o].[ContactPersonID], [ol].[Quantity] FROM [Sales].[Orders] [o] JOIN [Sales].[OrderLines] [ol] ON [o].[OrderID] = [ol].[OrderID] WHERE [OrderDate] BETWEEN @StartDate AND @EndDate ORDER BY [OrderDate] OPTION (RECOMPILE); GO
次に、前と同じ入力パラメーターを使用してプロシージャを再実行し、出力を確認します。
新しいquery_id(OPTION(RECOMPILE)を追加したためにクエリテキストが変更された)があり、2つの新しいplan_id値があり、プランの1つに異なるメモリ付与番号があることに注意してください。 plan_id 5の場合、実行は1つだけであり、メモリ付与番号は最初の実行と一致します。そのため、planは狭い日付範囲用です。 2つの大きな日付範囲で同じ計画が生成されましたが、メモリ付与には大きなばらつきがあります。最小で94,528、最大で573,568です。
クエリストアレポートを使用してメモリ付与情報を見ると、この変動性は少し異なって表示されます。データベースからTopResourceConsumersレポートを開き、メトリックをメモリ消費量(KB)と平均に変更すると、RECOMPILEを使用したクエリがリストの一番上に表示されます。
このウィンドウでは、メトリックは計画ではなくクエリによって集約されます。クエリストアビューに対して直接実行したクエリには、query_idだけでなくplan_idもリストされています。ここでは、クエリに2つのプランがあり、両方をプランの概要ウィンドウで表示できますが、このビューではすべてのプランのメトリックが結合されています。
ビューを直接見ると、メモリ付与の変動性は明らかです。統計をAvgからStDevに変更することで、UIを使用して変動性のあるクエリを見つけることができます:
クエリストアビューをクエリし、stdev_query_max_used_memoryの降順で並べ替えることで、同じ情報を見つけることができます。ただし、最小メモリ許可と最大メモリ許可の差、または差のパーセンテージに基づいて検索することもできます。たとえば、助成金の差が512 MBを超える場合について懸念がある場合は、次のように実行できます。
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;
列ストアインデックスを使用してSQLServer2017を実行している場合は、Memory Grantフィードバックを利用して、クエリストアでこの情報を使用することもできます。まず、Ordersテーブルを変更して、クラスター化された列ストアインデックスを追加します。
ALTER TABLE [Sales].[Invoices] DROP CONSTRAINT [FK_Sales_Invoices_OrderID_Sales_Orders]; GO ALTER TABLE [Sales].[Orders] 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];
次に、データベースの組み合わせ可能モードを140に設定して、メモリ付与フィードバックを活用できるようにします。
ALTER DATABASE [WideWorldImporters] SET COMPATIBILITY_LEVEL = 140; GO
最後に、ストアドプロシージャを変更して、クエリからOPTION(RECOMPILE)を削除し、さまざまな入力値を使用して数回実行します。
ALTER PROCEDURE [Sales].[usp_OrderInfo_OrderDate] @StartDate DATETIME, @EndDate DATETIME AS SELECT [o].[CustomerID], [o].[OrderDate], [o].[ContactPersonID], [ol].[Quantity] FROM [Sales].[Orders] [o] JOIN [Sales].[OrderLines] [ol] ON [o].[OrderID] = [ol].[OrderID] WHERE [OrderDate] BETWEEN @StartDate AND @EndDate ORDER BY [OrderDate]; 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'; GO
クエリストア内に次のように表示されます:
query_id =1の新しいプランがあります。これは、メモリ付与メトリックの値が異なり、plan_id 6の場合よりもStDevがわずかに低くなっています。クエリストアでプランを見ると、クラスター化された列ストアインデックスにアクセスしていることがわかります。 :
クエリストアの計画は実行されたものですが、見積もりのみが含まれていることに注意してください。プランキャッシュ内のプランでは、メモリフィードバックが発生したときにメモリ付与情報が更新されますが、この情報はクエリストアの既存のプランには適用されません。
概要
クエリストアを使用して可変メモリ許可のあるクエリを確認するのが好きな点は次のとおりです。データは自動的に収集されます。この問題が予期せず発生した場合は、情報を収集するために何も配置する必要はありません。すでにクエリストアにキャプチャされています。クエリがパラメータ化されている場合、プランのキャッシュのために静的な値が発生する可能性があるため、メモリ付与の変動性を見つけるのが難しい場合があります。ただし、再コンパイルにより、クエリに、問題を追跡するために使用できる非常に異なるメモリ許可値を持つ複数のプランがあることもわかります。クエリストアでキャプチャされたデータを使用して問題を調査するにはさまざまな方法があり、問題を事前に、また事後的に調べることができます。