Sqlserver
 sql >> Base de Dados >  >> RDS >> Sqlserver

Solucionando problemas de concessão de memória variável no SQL Server


Um dos problemas mais desconcertantes para solucionar problemas no SQL Server pode ser aqueles relacionados a concessões de memória. Algumas consultas precisam de mais memória do que outras para serem executadas, com base em quais operações precisam ser executadas (por exemplo, classificação, hash). O otimizador do SQL Server estima a quantidade de memória necessária e a consulta deve obter a concessão de memória para iniciar a execução. Ele mantém essa concessão durante a execução da consulta - o que significa que, se o otimizador superestimar a memória, você poderá ter problemas de simultaneidade. Se subestimar a memória, você poderá ver vazamentos no tempdb. Nenhum dos dois é o ideal e, quando você simplesmente tem muitas consultas solicitando mais memória do que a disponível para conceder, verá RESOURCE_SEMAPHORE aguardando. Existem várias maneiras de atacar esse problema, e um dos meus novos métodos favoritos é usar o Query Store.

Configuração


Usaremos uma cópia de WideWorldImporters que inflacionei usando o procedimento armazenado DataLoadSimulation.DailyProcessToCreateHistory. A tabela Sales.Orders tem cerca de 4,6 milhões de linhas e a tabela Sales.OrderLines tem cerca de 9,2 milhões de linhas. Restauraremos o backup e habilitaremos o Repositório de Consultas e limparemos todos os dados antigos do Repositório de Consultas para não alterar nenhuma métrica para esta demonstração.

Lembrete:Não execute ALTER DATABASE SET QUERY_STORE CLEAR; em seu banco de dados de produção, a menos que você queira remover tudo do 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

O procedimento armazenado que usaremos para testar as consultas das tabelas Orders e OrderLines acima mencionadas com base em um intervalo de datas:
  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

Teste


Vamos executar o procedimento armazenado com três conjuntos diferentes de parâmetros de entrada:
  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

A primeira execução retorna 1.958 linhas, a segunda retorna 267.268 linhas e a última retorna mais de 2,2 milhões de linhas. Se você observar os intervalos de datas, isso não é surpreendente – quanto maior o intervalo de datas, mais dados serão retornados.

Por se tratar de um procedimento armazenado, os parâmetros de entrada utilizados inicialmente determinam o plano, bem como a memória a ser concedida. Se observarmos o plano de execução real para a primeira execução, veremos loops aninhados e uma concessão de memória de 2656 KB.





As execuções subsequentes têm o mesmo plano (já que foi isso que foi armazenado em cache) e a mesma concessão de memória, mas temos uma pista de que não é suficiente porque há um aviso de classificação.



Se procurarmos no Repositório de Consultas por esse procedimento armazenado, veremos três execuções e os mesmos valores para a memória UsedKB, independentemente de examinarmos o Desvio Médio, Mínimo, Máximo, Último ou Padrão. Observação:as informações de concessão de memória no Query Store são relatadas como o número de páginas de 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 estivermos procurando por problemas de concessão de memória neste cenário – onde um plano é armazenado em cache e reutilizado – o Query Store não nos ajudará.

Mas e se a consulta específica for compilada na execução, seja por causa de uma dica RECOMPILE ou porque é ad-hoc?

Podemos alterar o procedimento para adicionar a dica RECOMPILE à instrução (o que é recomendado em vez de adicionar RECOMPILE no nível do procedimento ou executar o procedimento WITH RECOMPIPLE):
  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

Agora vamos reexecutar nosso procedimento com os mesmos parâmetros de entrada de antes e verificar a saída:



Observe que temos um novo query_id – o texto da consulta mudou porque adicionamos OPTION (RECOMPILE) a ele – e também temos dois novos valores de plan_id e temos diferentes números de concessão de memória para um de nossos planos. Para plan_id 5, há apenas uma execução e os números de concessão de memória correspondem à execução inicial – de modo que o plano é para o pequeno intervalo de datas. Os dois intervalos de data maiores geraram o mesmo plano, mas há uma variabilidade significativa nas concessões de memória – 94.528 para o mínimo e 573.568 para o máximo.

Se observarmos as informações de concessão de memória usando os relatórios do Repositório de Consultas, essa variabilidade será um pouco diferente. Abrindo o relatório Top Resource Consumers do banco de dados e, em seguida, alterando a métrica para Memory Consumption (KB) e Avg, nossa consulta com o RECOMPILE chega ao topo da lista.



Nesta janela, as métricas são agregadas por consulta, não por plano. A consulta que executamos diretamente nas visualizações do Query Store listou não apenas o query_id, mas também o plan_id. Aqui podemos ver que a consulta tem dois planos e podemos visualizar ambos na janela de resumo do plano, mas as métricas são combinadas para todos os planos nesta visualização.

A variabilidade nas concessões de memória é óbvia quando estamos olhando diretamente para as visualizações. Podemos encontrar consultas com variabilidade usando a interface do usuário alterando a estatística de Avg para StDev:



Podemos encontrar as mesmas informações consultando as visualizações do Query Store e ordenando por stdev_query_max_used_memory decrescente. Mas também podemos pesquisar com base na diferença entre a concessão de memória mínima e máxima, ou uma porcentagem da diferença. Por exemplo, se estivéssemos preocupados com casos em que a diferença nas concessões fosse maior que 512 MB, poderíamos executar:
  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;

Aqueles de vocês que executam o SQL Server 2017 com índices Columnstore, que têm a vantagem do feedback de concessão de memória, também podem usar essas informações no Repositório de Consultas. Primeiro, alteraremos nossa tabela Orders para adicionar um índice Columnstore clusterizado:
  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];

Em seguida, definiremos o modo de penteabilidade do banco de dados para 140 para que possamos aproveitar o feedback de concessão de memória:
  ALTER DATABASE [WideWorldImporters] SET COMPATIBILITY_LEVEL = 140;
  GO

Por fim, alteraremos nosso procedimento armazenado para remover OPTION (RECOMPILE) de nossa consulta e executá-lo algumas vezes com os diferentes valores de entrada:
  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

No Query Store, vemos o seguinte:



Temos um novo plano para query_id =1, que tem valores diferentes para as métricas de concessão de memória e um StDev um pouco menor do que tínhamos com plan_id 6. Se examinarmos o plano no Query Store, veremos que ele acessa o índice Columnstore clusterizado :



Lembre-se de que o plano no Query Store é aquele que foi executado, mas contém apenas estimativas. Embora o plano no cache do plano tenha informações de concessão de memória atualizadas quando ocorre o feedback de memória, essas informações não são aplicadas ao plano existente no Repositório de Consultas.

Resumo


Aqui está o que eu gosto de usar o Query Store para examinar consultas com concessões de memória variável:os dados estão sendo coletados automaticamente. Se esse problema aparecer inesperadamente, não precisamos fazer nada para tentar coletar informações, já o capturamos no Query Store. No caso em que uma consulta é parametrizada, pode ser mais difícil encontrar variabilidade de concessão de memória devido ao potencial de valores estáticos devido ao cache do plano. No entanto, também podemos descobrir que, devido à recompilação, a consulta possui vários planos com valores de concessão de memória extremamente diferentes que poderíamos usar para rastrear o problema. Há várias maneiras de investigar o problema usando os dados capturados no Repositório de Consultas e permite que você examine os problemas de forma proativa e reativa.