sql >> Base de Datos >  >> RDS >> Sqlserver

Solución de problemas de concesiones de memoria variable en SQL Server

Uno de los problemas más desconcertantes para solucionar en SQL Server puede ser el relacionado con las concesiones de memoria. Algunas consultas necesitan más memoria que otras para ejecutarse, según las operaciones que deben realizarse (por ejemplo, ordenar, hash). El optimizador de SQL Server estima cuánta memoria se necesita y la consulta debe obtener la concesión de memoria para comenzar a ejecutarse. Mantiene esa concesión durante la ejecución de la consulta, lo que significa que si el optimizador sobreestima la memoria, puede tener problemas de concurrencia. Si subestima la memoria, puede ver derrames en tempdb. Ninguno de los dos es ideal, y cuando simplemente tiene demasiadas consultas que solicitan más memoria de la que está disponible para otorgar, verá que RESOURCE_SEMAPHORE espera. Hay varias formas de atacar este problema, y ​​uno de mis nuevos métodos favoritos es usar Query Store.

Configuración

Usaremos una copia de WideWorldImporters que inflé usando el procedimiento almacenado DataLoadSimulation.DailyProcessToCreateHistory. La tabla Sales.Orders tiene alrededor de 4,6 millones de filas y la tabla Sales.OrderLines tiene alrededor de 9,2 millones de filas. Restauraremos la copia de seguridad y habilitaremos el Almacén de consultas, y eliminaremos los datos antiguos del Almacén de consultas para no alterar ninguna métrica para esta demostración.

Recordatorio:No ejecute ALTER DATABASE SET QUERY_STORE CLEAR; contra su base de datos de producción a menos que desee eliminar todo de 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

El procedimiento almacenado que usaremos para probar las consultas de las tablas Orders y OrderLines antes mencionadas en función de un intervalo de fechas:

  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

Pruebas

Ejecutaremos el procedimiento almacenado con tres 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

La primera ejecución devuelve 1958 filas, la segunda devuelve 267 268 filas y la última devuelve más de 2,2 millones de filas. Si observa los rangos de fechas, esto no es sorprendente:cuanto mayor sea el rango de fechas, más datos se devolverán.

Debido a que este es un procedimiento almacenado, los parámetros de entrada utilizados inicialmente determinan el plan, así como la memoria que se otorgará. Si observamos el plan de ejecución real para la primera ejecución, vemos bucles anidados y una concesión de memoria de 2656 KB.

Las ejecuciones posteriores tienen el mismo plan (ya que eso es lo que se almacenó en caché) y la misma concesión de memoria, pero tenemos una pista de que no es suficiente porque hay una advertencia de clasificación.

Si buscamos en Query Store este procedimiento almacenado, vemos tres ejecuciones y los mismos valores para la memoria de KB usados, ya sea que busquemos promedio, mínimo, máximo, último o desviación estándar. Nota:la información de concesión de memoria en Query Store se informa como el 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');

Si estamos buscando problemas de concesión de memoria en este escenario, donde un plan se almacena en caché y se reutiliza, Query Store no nos ayudará.

Pero, ¿qué sucede si la consulta específica se compila durante la ejecución, ya sea debido a una sugerencia RECOMPILE o porque es ad-hoc?

Podemos modificar el procedimiento para agregar la sugerencia RECOMPILE a la instrucción (que se recomienda en lugar de agregar RECOMPILE en el nivel de procedimiento o ejecutar el procedimiento 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

Ahora volveremos a ejecutar nuestro procedimiento con los mismos parámetros de entrada que antes y verificaremos la salida:

Tenga en cuenta que tenemos un nuevo query_id (el texto de consulta cambió porque le agregamos OPCIÓN (RECOMPILE)) y también tenemos dos nuevos valores de plan_id, y tenemos diferentes números de concesión de memoria para uno de nuestros planes. Para plan_id 5 solo hay una ejecución, y los números de concesión de memoria coinciden con la ejecución inicial, por lo que ese plan es para el rango de fechas pequeño. Los dos intervalos de fechas más grandes generaron el mismo plan, pero existe una variabilidad significativa en las concesiones de memoria:94 528 para el mínimo y 573 568 para el máximo.

Si observamos la información de concesión de memoria mediante los informes de Query Store, esta variabilidad se muestra un poco diferente. Al abrir el informe Principales consumidores de recursos de la base de datos y luego cambiar la métrica a Consumo de memoria (KB) y Promedio, nuestra consulta con RECOMPILE aparece en la parte superior de la lista.

En esta ventana, las métricas se agregan por consulta, no por plan. La consulta que ejecutamos directamente en las vistas del Almacén de consultas enumeraba no solo el query_id sino también el plan_id. Aquí podemos ver que la consulta tiene dos planes y podemos verlos en la ventana de resumen del plan, pero las métricas se combinan para todos los planes en esta vista.

La variabilidad en las concesiones de memoria es obvia cuando miramos directamente las vistas. Podemos encontrar consultas con variabilidad usando la interfaz de usuario cambiando la estadística de Avg a StDev:

Podemos encontrar la misma información consultando las vistas de Query Store y ordenando por stdev_query_max_used_memory descendente. Pero también podemos buscar en función de la diferencia entre la concesión de memoria mínima y máxima, o un porcentaje de la diferencia. Por ejemplo, si nos preocuparan los casos en los que la diferencia en las concesiones fuera superior a 512 MB, podríamos ejecutar:

  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;

Aquellos de ustedes que ejecutan SQL Server 2017 con índices de almacén de columnas, que tienen la ventaja de recibir comentarios sobre la concesión de memoria, también pueden usar esta información en el almacén de consultas. Primero cambiaremos nuestra tabla de pedidos para agregar un índice de almacén de columnas agrupado:

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

Luego, estableceremos el modo de combinación de la base de datos en 140 para que podamos aprovechar los comentarios sobre la concesión de memoria:

  ALTER DATABASE [WideWorldImporters] SET COMPATIBILITY_LEVEL = 140;
  GO

Finalmente, cambiaremos nuestro procedimiento almacenado para eliminar OPTION (RECOMPILE) de nuestra consulta y luego lo ejecutaremos varias veces con los 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

Dentro del Almacén de consultas vemos lo siguiente:

Tenemos un nuevo plan para query_id =1, que tiene diferentes valores para las métricas de concesión de memoria y un StDev ligeramente más bajo que el que teníamos con plan_id 6. Si miramos en el plan en Query Store, vemos que accede al índice agrupado de Columnstore. :

Recuerde que el plan en Query Store es el que se ejecutó, pero solo contiene estimaciones. Si bien el plan en la memoria caché del plan tiene información de concesión de memoria actualizada cuando se producen comentarios de memoria, esta información no se aplica al plan existente en Query Store.

Resumen

Esto es lo que me gusta de usar el Almacén de consultas para buscar consultas con concesiones de memoria variable:los datos se recopilan automáticamente. Si este problema aparece de forma inesperada, no tenemos que implementar nada para tratar de recopilar información, ya la tenemos capturada en Query Store. En el caso de que una consulta esté parametrizada, puede ser más difícil encontrar la variabilidad de la concesión de memoria debido a la posibilidad de valores estáticos debido al almacenamiento en caché del plan. Sin embargo, también podemos descubrir que, debido a la recompilación, la consulta tiene varios planes con valores de concesión de memoria extremadamente diferentes que podríamos usar para rastrear el problema. Hay una variedad de formas de investigar el problema utilizando los datos capturados en Query Store, y le permite ver los problemas de manera proactiva y reactiva.