sql >> Databáze >  >> RDS >> Sqlserver

Odstraňování problémů s přidělením proměnné paměti na serveru SQL Server

Jedním z více matoucích problémů k odstraňování problémů na serveru SQL Server mohou být problémy související s přidělením paměti. Některé dotazy potřebují ke spuštění více paměti než jiné, podle toho, jaké operace je třeba provést (např. řazení, hash). Optimalizátor SQL Serveru odhadne, kolik paměti je potřeba, a dotaz musí získat přidělení paměti, aby se mohl spustit. Drží toto povolení po dobu provádění dotazu – což znamená, že pokud optimalizátor nadhodnocuje paměť, můžete narazit na problémy se souběžností. Pokud podceňuje paměť, můžete v tempdb vidět úniky. Ani jedno není ideální, a když máte jednoduše příliš mnoho dotazů požadujících více paměti, než je k dispozici pro udělení, uvidíte, že RESOURCE_SEMAPHORE čeká. Existuje několik způsobů, jak na tento problém zaútočit, a jednou z mých nových oblíbených metod je použití Query Store.

Nastavení

Použijeme kopii WideWorldImporters, kterou jsem nafoukl pomocí uložené procedury DataLoadSimulation.DailyProcessToCreateHistory. Tabulka Sales.Orders má přibližně 4,6 milionu řádků a tabulka Sales.OrderLines má přibližně 9,2 milionu řádků. Obnovíme zálohu a povolíme Query Store a vymažeme všechna stará data Query Store, abychom pro tuto ukázku neměnili žádné metriky.

Připomenutí:Nespouštějte ALTER DATABASE SET QUERY_STORE CLEAR; proti vaší produkční databázi, pokud nechcete z Query Store odebrat vše.

  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

Uložená procedura, kterou použijeme k testování, se dotazuje na výše uvedené tabulky Orders a OrderLines na základě časového rozsahu:

  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

Testování

Provedeme uloženou proceduru se třemi různými sadami vstupních parametrů:

  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

První provedení vrátí 1958 řádků, druhé vrátí 267 268 řádků a poslední vrátí více než 2,2 milionu řádků. Pokud se podíváte na časová období, není to překvapivé – čím větší je časové období, tím více dat se vrací.

Protože se jedná o uloženou proceduru, použité vstupní parametry zpočátku určují plán a také přidělenou paměť. Pokud se podíváme na skutečný plán provádění pro první spuštění, vidíme vnořené smyčky a přidělení paměti 2656 KB.

Následná spuštění mají stejný plán (jako to, co bylo uloženo do mezipaměti) a stejné přidělení paměti, ale získáme vodítko, že to nestačí, protože existuje varování o řazení.

Pokud se podíváme do Query Store na tuto uloženou proceduru, vidíme tři spuštění a stejné hodnoty pro UsedKB memory, ať už se podíváme na Průměr, Minimum, Maximum, Poslední nebo Standardní odchylka. Poznámka:Informace o přidělení paměti v úložišti dotazů jsou hlášeny jako počet stránek o velikosti 8 kB.

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

Pokud v tomto scénáři hledáme problémy s přidělením paměti – kde je plán uložen do mezipaměti a znovu použit – Query Store nám nepomůže.

Ale co když je konkrétní dotaz zkompilován při spuštění, buď kvůli nápovědě RECOMPILE, nebo protože je to ad-hoc?

Můžeme změnit proceduru tak, aby do příkazu byla přidána nápověda RECOMPILE (což se doporučuje před přidáním RECOMPILE na úrovni procedury nebo spuštěním procedury 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

Nyní znovu spustíme naši proceduru se stejnými vstupními parametry jako dříve a zkontrolujeme výstup:

Všimněte si, že máme nové query_id – text dotazu se změnil, protože jsme do něj přidali OPTION (RECOMPILE) – a také máme dvě nové hodnoty plan_id a pro jeden z našich plánů máme různá čísla přidělení paměti. Pro plan_id 5 je pouze jedno provedení a čísla přidělení paměti odpovídají počátečnímu provedení – takže plán je pro malé časové období. Dva větší rozsahy dat vytvořily stejný plán, ale existuje značná variabilita v přidělení paměti – 94 528 pro minimum a 573 568 pro maximum.

Pokud se podíváme na informace o přidělení paměti pomocí sestav Query Store, tato variabilita se ukáže trochu jinak. Otevřením přehledu Top Resource Consumers z databáze a následnou změnou metriky na Memory Consumption (KB) a Avg se náš dotaz s RECOMPILE dostane na začátek seznamu.

V tomto okně jsou metriky agregovány podle dotazu, nikoli podle plánu. Dotaz, který jsme provedli přímo proti zobrazením Query Store, neuváděl pouze query_id, ale také plan_id. Zde vidíme, že dotaz má dva plány a oba je můžeme zobrazit v okně souhrnu plánu, ale metriky jsou v tomto zobrazení kombinovány pro všechny plány.

Variabilita v přidělení paměti je zřejmá, když se díváme přímo na pohledy. Dotazy s variabilitou můžeme najít pomocí uživatelského rozhraní změnou statistiky z Avg na StDev:

Stejné informace můžeme najít dotazem na zobrazení Query Store a seřazením podle stdev_query_max_used_memory sestupně. Můžeme však také vyhledávat na základě rozdílu mezi minimálním a maximálním přidělením paměti nebo procenta rozdílu. Pokud bychom se například obávali případů, kdy byl rozdíl v grantech větší než 512 MB, mohli bychom spustit:

  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;

Ti z vás, kteří používají SQL Server 2017 s indexy Columnstore, kteří mají výhodu zpětné vazby Memory Grant, mohou tyto informace také použít v Query Store. Nejprve změníme naši tabulku Orders tak, aby přidala seskupený index 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];

Poté nastavíme režim česání databáze na 140, abychom mohli využít zpětnou vazbu o přidělení paměti:

  ALTER DATABASE [WideWorldImporters] SET COMPATIBILITY_LEVEL = 140;
  GO

Nakonec změníme naši uloženou proceduru tak, abychom z našeho dotazu odstranili OPTION (RECOMPILE) a poté ji několikrát spustili s různými vstupními hodnotami:

  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

V obchodě dotazů vidíme následující:

Máme nový plán pro query_id =1, který má jiné hodnoty pro metriky přidělení paměti a o něco nižší StDev, než jsme měli s plan_id 6. Pokud se podíváme do plánu v Query Store, vidíme, že přistupuje ke clusterovanému indexu Columnstore :

Pamatujte, že plán v Query Store je ten, který byl proveden, ale obsahuje pouze odhady. Zatímco plán v mezipaměti plánu má informace o přidělení paměti aktualizované, když dojde k zpětné vazbě paměti, tyto informace se nepoužijí na existující plán v Query Store.

Shrnutí

Zde je to, co se mi líbí na používání Query Store k prohlížení dotazů s variabilními paměťovými granty:data se automaticky shromažďují. Pokud se tento problém objeví neočekávaně, nemusíme nic zavádět, abychom se pokusili shromáždit informace, již je máme zachycené v Query Store. V případě, kdy je dotaz parametrizován, může být obtížnější najít variabilitu přidělení paměti kvůli potenciálu statických hodnot kvůli ukládání plánu do mezipaměti. Můžeme však také zjistit, že v důsledku rekompilace má dotaz více plánů s extrémně odlišnými hodnotami přidělení paměti, které bychom mohli použít ke sledování problému. Existuje celá řada způsobů, jak problém prozkoumat pomocí dat zachycených v Query Store, a to vám umožní podívat se na problémy proaktivně i reaktivně.


  1. Jak identifikovat duplikaci primárního klíče z kódu chyby SQL Server 2008?

  2. 4 způsoby, jak zkontrolovat typ dat sloupce v MariaDB

  3. Jak mohu použít PDO k načtení pole výsledků v PHP?

  4. Jak zapíšu data z R do tabulek PostgreSQL s automaticky se zvyšujícím primárním klíčem?