Sqlserver
 sql >> Database >  >> RDS >> Sqlserver

Risoluzione dei problemi relativi alle concessioni di memoria variabile in SQL Server

Uno dei problemi più difficili da risolvere in SQL Server può essere quello relativo alle concessioni di memoria. Alcune query richiedono più memoria di altre per essere eseguite, in base alle operazioni che devono essere eseguite (ad esempio ordinamento, hash). L'ottimizzatore di SQL Server stima la quantità di memoria necessaria e la query deve ottenere la concessione di memoria per avviare l'esecuzione. Mantiene tale concessione per la durata dell'esecuzione della query, il che significa che se l'ottimizzatore sovrastima la memoria è possibile incorrere in problemi di concorrenza. Se sottovaluta la memoria, puoi vedere gli spill in tempdb. Nessuno dei due è l'ideale e quando hai semplicemente troppe query che richiedono più memoria di quella disponibile per concedere, vedrai RESOURCE_SEMAPHORE attese. Esistono diversi modi per affrontare questo problema e uno dei miei nuovi metodi preferiti è utilizzare Query Store.

Configurazione

Useremo una copia di WideWorldImporters che ho gonfiato usando la procedura memorizzata DataLoadSimulation.DailyProcessToCreateHistory. La tabella Sales.Orders ha circa 4,6 milioni di righe e la tabella Sales.OrderLines ha circa 9,2 milioni di righe. Ripristineremo il backup e abiliteremo Query Store e cancelleremo tutti i vecchi dati di Query Store in modo da non alterare le metriche per questa demo.

Promemoria:non eseguire ALTER DATABASE SET QUERY_STORE CLEAR; rispetto al database di produzione, a meno che non desideri rimuovere tutto da 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

La procedura memorizzata che utilizzeremo per testare le query sulle suddette tabelle Orders e OrderLines in base a un intervallo di date:

  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

Test

Eseguiremo la procedura memorizzata con tre diversi set di parametri di input:

  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

La prima esecuzione restituisce 1958 righe, la seconda restituisce 267.268 righe e l'ultima restituisce oltre 2,2 milioni di righe. Se osservi gli intervalli di date, ciò non sorprende:maggiore è l'intervallo di date, maggiore è la quantità di dati restituiti.

Poiché si tratta di una procedura memorizzata, i parametri di input utilizzati inizialmente determinano il piano, nonché la memoria da concedere. Se osserviamo il piano di esecuzione effettivo per la prima esecuzione, vediamo loop nidificati e una concessione di memoria di 2656 KB.

Le esecuzioni successive hanno lo stesso piano (poiché è quello che è stato memorizzato nella cache) e la stessa concessione di memoria, ma abbiamo un indizio che non è abbastanza perché c'è un avviso di tipo.

Se esaminiamo questa procedura memorizzata in Query Store, vediamo tre esecuzioni e gli stessi valori per la memoria UsedKB, indipendentemente dal fatto che osserviamo la deviazione media, minima, massima, ultima o standard. Nota:le informazioni sulla concessione di memoria in Query Store sono riportate come numero di pagine da 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');

Se stiamo cercando problemi di concessione della memoria in questo scenario, in cui un piano viene memorizzato nella cache e riutilizzato, Query Store non ci aiuterà.

Ma cosa succede se la query specifica viene compilata durante l'esecuzione, a causa di un suggerimento RECOMPILE o perché è ad hoc?

Possiamo modificare la procedura per aggiungere l'hint RECOMPILE all'istruzione (che è consigliato aggiungere RECOMPILE a livello di procedura o eseguire la procedura CON 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

Ora eseguiremo nuovamente la nostra procedura con gli stessi parametri di input di prima e controlleremo l'output:

Si noti che abbiamo un nuovo query_id - il testo della query è cambiato perché abbiamo aggiunto OPTION (RICIMPILA) ad esso - e abbiamo anche due nuovi valori plan_id e abbiamo diversi numeri di concessione della memoria per uno dei nostri piani. Per plan_id 5 c'è solo un'esecuzione e i numeri di concessione della memoria corrispondono all'esecuzione iniziale, quindi il piano è per l'intervallo di date piccolo. I due intervalli di date più grandi hanno generato lo stesso piano, ma c'è una variabilità significativa nelle concessioni di memoria:94.528 per il minimo e 573.568 per il massimo.

Se osserviamo le informazioni sulla concessione della memoria utilizzando i rapporti Query Store, questa variabilità si presenta in modo leggermente diverso. Aprendo il rapporto Principali consumatori di risorse dal database e quindi modificando la metrica in Consumo di memoria (KB) e Media, la nostra query con RECOMPILE arriva in cima all'elenco.

In questa finestra, le metriche vengono aggregate per query, non per piano. La query eseguita direttamente sulle viste Query Store elencava non solo query_id ma anche plan_id. Qui possiamo vedere che la query ha due piani e possiamo visualizzarli entrambi nella finestra di riepilogo del piano, ma le metriche sono combinate per tutti i piani in questa vista.

La variabilità nelle concessioni di memoria è ovvia quando osserviamo direttamente le viste. Possiamo trovare query con variabilità utilizzando l'interfaccia utente modificando la statistica da Avg a StDev:

Possiamo trovare le stesse informazioni interrogando le viste del Query Store e ordinando per stdev_query_max_used_memory decrescente. Ma possiamo anche cercare in base alla differenza tra la concessione di memoria minima e massima o una percentuale della differenza. Ad esempio, se fossimo preoccupati per casi in cui la differenza nelle sovvenzioni fosse maggiore di 512 MB, potremmo eseguire:

  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;

Quelli di voi che eseguono SQL Server 2017 con indici Columnstore, che hanno il vantaggio del feedback sulla concessione della memoria, possono anche usare queste informazioni in Query Store. Per prima cosa cambieremo la nostra tabella Ordini per aggiungere un indice Columnstore in cluster:

  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];

Quindi imposteremo la modalità di combability del database su 140 in modo da poter sfruttare il feedback sulla concessione della memoria:

  ALTER DATABASE [WideWorldImporters] SET COMPATIBILITY_LEVEL = 140;
  GO

Infine, cambieremo la nostra procedura memorizzata per rimuovere OPTION (RICIMPILA) dalla nostra query e quindi eseguirla alcune volte con i diversi valori di input:

  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

All'interno di Query Store vediamo quanto segue:

Abbiamo un nuovo piano per query_id =1, che ha valori diversi per le metriche di concessione della memoria e un StDev leggermente inferiore a quello che avevamo con plan_id 6. Se osserviamo il piano in Query Store, vediamo che accede all'indice Columnstore cluster :

Ricorda che il piano in Query Store è quello che è stato eseguito, ma contiene solo stime. Sebbene il piano nella cache del piano abbia informazioni sulla concessione della memoria aggiornate quando si verifica il feedback sulla memoria, queste informazioni non vengono applicate al piano esistente in Query Store.

Riepilogo

Ecco cosa mi piace dell'utilizzo di Query Store per esaminare le query con concessioni di memoria variabile:i dati vengono raccolti automaticamente. Se questo problema si presenta inaspettatamente, non dobbiamo mettere in atto nulla per provare a raccogliere informazioni, le abbiamo già acquisite in Query Store. Nel caso in cui una query sia parametrizzata, potrebbe essere più difficile trovare la variabilità della concessione di memoria a causa del potenziale di valori statici a causa della memorizzazione nella cache del piano. Tuttavia, potremmo anche scoprire che, a causa della ricompilazione, la query ha più piani con valori di concessione della memoria estremamente diversi che potremmo utilizzare per rintracciare il problema. Esistono vari modi per indagare sul problema utilizzando i dati acquisiti in Query Store e ti consente di esaminare i problemi in modo proattivo e reattivo.