Sqlserver
 sql >> Base de données >  >> RDS >> Sqlserver

Dépannage des allocations de mémoire variables dans SQL Server

L'un des problèmes les plus complexes à résoudre dans SQL Server peut être celui lié aux allocations de mémoire. Certaines requêtes nécessitent plus de mémoire que d'autres pour s'exécuter, en fonction des opérations à effectuer (par exemple, trier, hacher). L'optimiseur de SQL Server estime la quantité de mémoire nécessaire et la requête doit obtenir l'allocation de mémoire pour commencer à s'exécuter. Il conserve cette autorisation pendant toute la durée de l'exécution de la requête, ce qui signifie que si l'optimiseur surestime la mémoire, vous pouvez rencontrer des problèmes de concurrence. S'il sous-estime la mémoire, vous pouvez voir des déversements dans tempdb. Ni l'un ni l'autre n'est idéal, et lorsque vous avez simplement trop de requêtes demandant plus de mémoire que ce qui est disponible pour l'octroi, vous verrez RESOURCE_SEMAPHORE attendre. Il existe plusieurs façons d'attaquer ce problème, et l'une de mes nouvelles méthodes préférées consiste à utiliser Query Store.

Configuration

Nous allons utiliser une copie de WideWorldImporters que j'ai gonflée à l'aide de la procédure stockée DataLoadSimulation.DailyProcessToCreateHistory. La table Sales.Orders contient environ 4,6 millions de lignes et la table Sales.OrderLines environ 9,2 millions de lignes. Nous allons restaurer la sauvegarde et activer Query Store, et effacer toutes les anciennes données Query Store afin de ne modifier aucune métrique pour cette démo.

Rappel :n'exécutez pas ALTER DATABASE SET QUERY_STORE CLEAR ; par rapport à votre base de données de production, sauf si vous souhaitez tout supprimer 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

La procédure stockée que nous utiliserons pour tester interroge les tables Orders et OrderLines susmentionnées en fonction d'une plage de dates :

  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

Nous allons exécuter la procédure stockée avec trois ensembles différents de paramètres d'entrée :

  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 première exécution renvoie 1958 lignes, la seconde renvoie 267 268 lignes et la dernière renvoie plus de 2,2 millions de lignes. Si vous regardez les plages de dates, ce n'est pas surprenant :plus la plage de dates est large, plus le nombre de données renvoyées est important.

Comme il s'agit d'une procédure stockée, les paramètres d'entrée utilisés initialement déterminent le plan, ainsi que la mémoire à allouer. Si nous regardons le plan d'exécution réel pour la première exécution, nous voyons des boucles imbriquées et une allocation de mémoire de 2 656 Ko.

Les exécutions suivantes ont le même plan (car c'est ce qui a été mis en cache) et la même allocation de mémoire, mais nous obtenons un indice que ce n'est pas suffisant car il y a un avertissement de tri.

Si nous regardons dans le magasin de requêtes pour cette procédure stockée, nous voyons trois exécutions et les mêmes valeurs pour la mémoire UsedKB, que nous examinions la moyenne, le minimum, le maximum, le dernier ou l'écart type. Remarque :les informations d'allocation de mémoire dans le magasin de requêtes sont signalées sous la forme d'un nombre de pages de 8 Ko.

  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 nous recherchons des problèmes d'allocation de mémoire dans ce scénario - où un plan est mis en cache et réutilisé - Query Store ne nous aidera pas.

Mais que se passe-t-il si la requête spécifique est compilée à l'exécution, soit à cause d'un indice RECOMPILE, soit parce qu'elle est ad hoc ?

Nous pouvons modifier la procédure pour ajouter l'indicateur RECOMPILE à l'instruction (ce qui est recommandé plutôt que d'ajouter RECOMPILE au niveau de la procédure ou d'exécuter la procédure 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

Nous allons maintenant relancer notre procédure avec les mêmes paramètres d'entrée qu'auparavant, et vérifier la sortie :

Notez que nous avons un nouveau query_id - le texte de la requête a changé car nous y avons ajouté OPTION (RECOMPILE) - et nous avons également deux nouvelles valeurs plan_id, et nous avons des numéros d'attribution de mémoire différents pour l'un de nos plans. Pour plan_id 5, il n'y a qu'une seule exécution et les numéros d'attribution de mémoire correspondent à l'exécution initiale, de sorte que le plan concerne la petite plage de dates. Les deux plages de dates plus larges ont généré le même plan, mais il existe une variabilité significative dans les allocations de mémoire :94 528 pour le minimum et 573 568 pour le maximum.

Si nous examinons les informations d'allocation de mémoire à l'aide des rapports du magasin de requêtes, cette variabilité apparaît un peu différemment. En ouvrant le rapport Principaux consommateurs de ressources à partir de la base de données, puis en modifiant la métrique en Consommation de mémoire (Ko) et Moy, notre requête avec RECOMPILE arrive en haut de la liste.

Dans cette fenêtre, les mesures sont agrégées par requête, et non par plan. La requête que nous avons exécutée directement sur les vues du magasin de requêtes répertorie non seulement le query_id mais aussi le plan_id. Ici, nous pouvons voir que la requête a deux plans, et nous pouvons les afficher tous les deux dans la fenêtre de résumé du plan, mais les métriques sont combinées pour tous les plans dans cette vue.

La variabilité des allocations de mémoire est évidente lorsque nous regardons directement les vues. Nous pouvons trouver des requêtes avec variabilité à l'aide de l'interface utilisateur en modifiant la statistique de Avg à StDev :

Nous pouvons trouver les mêmes informations en interrogeant les vues du magasin de requêtes et en les classant par stdev_query_max_used_memory décroissant. Mais, nous pouvons également effectuer une recherche en fonction de la différence entre l'allocation de mémoire minimale et maximale, ou d'un pourcentage de la différence. Par exemple, si nous étions préoccupés par les cas où la différence dans les subventions était supérieure à 512 Mo, nous pourrions exécuter :

  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;

Ceux d'entre vous qui exécutent SQL Server 2017 avec des index Columnstore, qui bénéficient des commentaires de Memory Grant, peuvent également utiliser ces informations dans Query Store. Nous allons d'abord modifier notre table Orders pour ajouter un index Columnstore 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];

Ensuite, nous définirons le mode de combabilité de la base de données sur 140 afin de pouvoir tirer parti des commentaires sur l'octroi de mémoire :

  ALTER DATABASE [WideWorldImporters] SET COMPATIBILITY_LEVEL = 140;
  GO

Enfin, nous allons modifier notre procédure stockée pour supprimer OPTION (RECOMPILE) de notre requête, puis l'exécuter plusieurs fois avec les différentes valeurs d'entrée :

  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

Dans Query Store, nous voyons ce qui suit :

Nous avons un nouveau plan pour query_id =1, qui a des valeurs différentes pour les métriques d'allocation de mémoire, et un StDev légèrement inférieur à celui que nous avions avec plan_id 6. Si nous regardons dans le plan dans Query Store, nous voyons qu'il accède à l'index clustered Columnstore :

N'oubliez pas que le plan dans Query Store est celui qui a été exécuté, mais il ne contient que des estimations. Bien que le plan dans le cache du plan ait des informations d'allocation de mémoire mises à jour lorsque le retour de mémoire se produit, ces informations ne sont pas appliquées au plan existant dans le magasin de requêtes.

Résumé

Voici ce que j'aime dans l'utilisation de Query Store pour examiner les requêtes avec des allocations de mémoire variables :les données sont automatiquement collectées. Si ce problème survient de manière inattendue, nous n'avons rien à mettre en place pour essayer de collecter des informations, nous les avons déjà capturées dans Query Store. Dans le cas où une requête est paramétrée, il peut être plus difficile de trouver la variabilité de l'allocation de mémoire en raison du potentiel de valeurs statiques en raison de la mise en cache du plan. Cependant, nous pouvons également découvrir qu'en raison de la recompilation, la requête a plusieurs plans avec des valeurs d'allocation de mémoire extrêmement différentes que nous pourrions utiliser pour rechercher le problème. Il existe plusieurs façons d'étudier le problème à l'aide des données capturées dans le magasin de requêtes, et cela vous permet d'examiner les problèmes de manière proactive et réactive.