Sqlserver
 sql >> база данни >  >> RDS >> Sqlserver

Отстраняване на неизправности при предоставяне на променлива памет в SQL Server

Един от по-обърканите проблеми за отстраняване на неизправности в SQL Server могат да бъдат тези, свързани с предоставянето на памет. Някои заявки се нуждаят от повече памет, отколкото други за изпълнение, въз основа на това какви операции трябва да се извършат (например сортиране, хеширане). Оптимизаторът на SQL Server оценява колко памет е необходима и заявката трябва да получи разрешение за памет, за да започне да се изпълнява. Той притежава това разрешение за продължителността на изпълнение на заявката - което означава, че ако оптимизаторът надценява паметта, можете да срещнете проблеми с едновременността. Ако подценява паметта, тогава можете да видите разливи в tempdb. Нито едно от двете не е идеално и когато просто имате твърде много заявки, изискващи повече памет, отколкото е налична за предоставяне, ще видите RESOURCE_SEMAPHORE чака. Има няколко начина за атака на този проблем и един от новите ми любими методи е да използвам Query Store.

Настройка

Ще използваме копие на WideWorldImporters, което надух с помощта на съхранената процедура DataLoadSimulation.DailyProcessToCreateHistory. Таблицата Sales.Orders има около 4,6 милиона реда, а таблицата Sales.OrderLines има около 9,2 милиона реда. Ще възстановим архива и ще активираме Query Store и ще изчистим всички стари данни от Query Store, така че да не променяме никакви показатели за тази демонстрация.

Напомняне:Не стартирайте ALTER DATABASE SET QUERY_STORE CLEAR; срещу вашата производствена база данни, освен ако не искате да премахнете всичко от Query Store.

  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

Тестване

Ще изпълним съхранената процедура с три различни набора от входни параметри:

  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 реда, второто връща 267 268 реда, а последното връща над 2,2 милиона реда. Ако погледнете периодите от време, това не е изненадващо – колкото по-голям е периодът от време, толкова повече данни се връщат.

Тъй като това е съхранена процедура, използваните входни параметри първоначално определят плана, както и паметта, която ще бъде предоставена. Ако погледнем действителния план за изпълнение за първото изпълнение, ще видим вложени цикли и предоставена памет от 2656 KB.

Последващите изпълнения имат същия план (тъй като това е кеширано) и същото разрешение за памет, но получаваме представа, че не е достатъчно, защото има предупреждение за сортиране.

Ако потърсим в хранилището на заявки за тази съхранена процедура, ще видим три изпълнения и същите стойности за UsedKB памет, независимо дали разглеждаме Average, Minimum, Maximum, Last или Standard Deviation. Забележка:информацията за предоставяне на памет в хранилището на заявки се отчита като броя на страниците от 8 КБ.

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

Ако търсим проблеми с предоставянето на памет в този сценарий – когато планът се кешира и използва повторно – Query Store няма да ни помогне.

Но какво ще стане, ако конкретната заявка се компилира при изпълнение или поради намек за ПРЕКОМПИЛИРАНЕ, или защото е ad-hoc?

Можем да променим процедурата, за да добавим намек за RECOMPILE към изявлението (което се препоръчва вместо добавянето на RECOMPILE на ниво процедура или изпълняването на процедурата WITH 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]
  OPTION (RECOMPILE);
  GO

Сега ще стартираме отново нашата процедура със същите входни параметри, както преди, и ще проверим изхода:

Забележете, че имаме нов query_id – текстът на заявката се промени, защото добавихме OPTION (RECOMPILE) към него – и също така имаме две нови стойности на plan_id и имаме различни номера на предоставяне на памет за един от нашите планове. За plan_id 5 има само едно изпълнение и номерата на предоставената памет съответстват на първоначалното изпълнение – така че този план е за малкия период от време. Двата по-големи диапазона от време генерират един и същ план, но има значителна променливост в предоставените памети - 94 528 за минимум и 573 568 за максимум.

Ако разгледаме информацията за предоставяне на памет с помощта на отчетите за хранилището на заявки, тази променливост се проявява малко по-различно. Отваряйки отчета за най-големите потребители на ресурси от базата данни и след това промените показателя на Консумация на памет (KB) и средно, нашата заявка с RECOMPILE излиза в началото на списъка.

В този прозорец показателите се обобщават по заявка, а не по план. Заявката, която изпълнихме директно срещу изгледите на Query Store, изброи не само query_id, но и plan_id. Тук можем да видим, че заявката има два плана и можем да ги видим и двата в прозореца за обобщение на плана, но показателите се комбинират за всички планове в този изглед.

Променливостта в предоставянето на памет е очевидна, когато гледаме директно изгледите. Можем да намерим заявки с променливост, използвайки потребителския интерфейс, като променим статистиката от Avg на StDev:

Можем да намерим същата информация, като запитаме изгледите на хранилището на заявки и подредим по низходящ stdev_query_max_used_memory. Но можем също да търсим въз основа на разликата между минималното и максималното предоставяне на памет или процент от разликата. Например, ако бяхме загрижени за случаи, при които разликата в безвъзмездните средства е по-голяма от 512MB, бихме могли да изпълним:

  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;

Тези от вас, които използват SQL Server 2017 с индекси на Columnstore, които имат предимството на обратната връзка с Memory Grant, също могат да използват тази информация в Query Store. Първо ще променим нашата таблица с поръчки, за да добавим клъстериран индекс на Columnstore:

  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, който има различни стойности за показателите за предоставяне на памет и малко по-ниско StDev, отколкото имахме с plan_id 6. Ако погледнем в плана в Query Store, виждаме, че има достъп до клъстерирания индекс на Columnstore :

Не забравяйте, че планът в Query Store е този, който е изпълнен, но съдържа само прогнози. Докато планът в кеша на плана има информация за предоставяне на памет, актуализирана, когато възникне обратна връзка с паметта, тази информация не се прилага към съществуващия план в хранилището на заявки.

Резюме

Ето какво харесвам в използването на Query Store за разглеждане на заявки с променлива памет:данните се събират автоматично. Ако този проблем се появи неочаквано, не е нужно да поставяме нищо, за да се опитаме да съберем информация, вече я имаме заснета в хранилището на заявки. В случай, когато заявка е параметризирана, може да е по-трудно да се намери променливост на предоставяне на памет поради потенциала за статични стойности поради кеширане на плана. Въпреки това можем също да открием, че поради прекомпилиране, заявката има множество планове с изключително различни стойности за предоставяне на памет, които бихме могли да използваме, за да проследим проблема. Има различни начини за изследване на проблема с помощта на данните, заснети в хранилището на заявки, и ви позволява да разглеждате проблемите проактивно, както и реактивно.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. SQL, Помощна таблица с числа

  2. Каква е разликата между char, nchar, varchar и nvarchar в SQL Server?

  3. Как работи sys.dm_exec_describe_first_result_set в SQL Server

  4. Променете формата на датата за текущата сесия в SQL Server

  5. Параметризираната заявка очаква параметъра, който не е бил предоставен