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

Utilisation de DBCC CLONEDATABASE et du magasin de requêtes pour les tests

L'été dernier, après la sortie de SP2 pour SQL Server 2014, j'ai écrit sur l'utilisation de DBCC CLONEDATABASE pour plus que simplement enquêter sur un problème de performances de requête. Un commentaire récent sur le message par un lecteur m'a fait penser que je devrais développer ce que j'avais en tête sur la façon d'utiliser la base de données clonée pour les tests. Pierre a écrit :

"Je suis principalement un développeur C # et même si j'écris et traite tout le temps avec T-SQL lorsqu'il s'agit d'aller au-delà de ce serveur SQL (à peu près tous les trucs DBA, les statistiques et autres), je ne sais pas vraiment grand-chose . Je ne sais même pas vraiment comment j'utiliserais une base de données clone comme celle-ci pour le réglage des performances »

Eh bien Pierre, voilà. J'espère que cela vous aidera !

Configuration

DBCC CLONEDATABASE a été mis à disposition dans SQL Server 2016 SP1, c'est donc ce que nous utiliserons pour les tests car il s'agit de la version actuelle et parce que je peux utiliser Query Store pour capturer mes données. Pour me faciliter la vie, je crée une base de données pour les tests, plutôt que de restaurer un échantillon de Microsoft.

USE [master];GO DROP DATABASE IF EXISTS [CustomerDB], [CustomerDB_CLONE];GO /* Modifier les emplacements des fichiers selon les besoins */ CREATE DATABASE [CustomerDB] ON PRIMARY ( NAME =N'CustomerDB', FILENAME =N' C:\Databases\CustomerDB.mdf' , SIZE =512MB , MAXSIZE =UNLIMITED, FILEGROWTH =65536KB ) LOG ON ( NAME =N'CustomerDB_log', FILENAME =N'C:\Databases\CustomerDB_log.ldf' , SIZE =512MB , MAXSIZE =UNLIMITED , FILEGROWTH =65536KB );GO ALTER DATABASE [CustomerDB] SET RECOVERY SIMPLE;

Maintenant, créez un tableau et ajoutez des données :

USE [CustomerDB];GO CREATE TABLE [dbo].[Customers]( [CustomerID] [int] NOT NULL, [FirstName] [nvarchar](64) NOT NULL, [LastName] [nvarchar](64) NOT NULL, [EMail] [nvarchar](320) NOT NULL, [Active] [bit] NOT NULL DEFAULT 1, [Créé] [datetime] NOT NULL DEFAULT SYSDATETIME(), [Mise à jour] [datetime] NULL, CONTRAINTE [PK_Customers] PRIMARY KEY CLUSTERED ([CustomerID]));GO /* Cela ajoute 1 000 000 lignes à la table ; n'hésitez pas à ajouter less*/INSERT dbo.Customers WITH (TABLOCKX) (CustomerID, FirstName, LastName, EMail, [Active]) SELECT rn =ROW_NUMBER() OVER (ORDER BY n), fn, ln, em, a FROM ( SELECT TOP (1000000) fn, ln, em, a =MAX(a), n =MAX(NEWID()) FROM ( SELECT fn, ln, em, a, r =ROW_NUMBER() OVER (PARTITION BY em ORDER BY em ) FROM ( SELECT TOP (20000000) fn =LEFT(o.name, 64), ln =LEFT(c.name, 64), em =LEFT(o.name, LEN(c.name)%5+1) + '.' + GAUCHE(c.nom, LEN(o.nom)%5+2) + '@' + DROITE(c.nom, LEN(o.nom + c.nom)%12 + 1) + GAUCHE( RTRIM(CHECKSUM(NEWID())),3) + '.com', a =CASE WHEN c.name LIKE '%y%' THEN 0 ELSE 1 END FROM sys.all_objects AS o CROSS JOIN sys.all_columns AS c ORDER BY NEWID() ) AS x ) AS y WHERE r =1 GROUP BY fn, ln, em ORDER BY n ) AS z ORDER BY rn;GO CREATE NONCLUSTERED INDEX [PhoneBook_Customers] ON [dbo].[Customers]([LastName] ,[Prénom])INCLUDE ([Email]);

Maintenant, nous allons activer Query Store :

USE [master];GO ALTER DATABASE [CustomerDB] SET QUERY_STORE =ON; ALTER DATABASE [CustomerDB] SET QUERY_STORE ( OPERATION_MODE =READ_WRITE, CLEANUP_POLICY =(STALE_QUERY_THRESHOLD_DAYS =30), DATA_FLUSH_INTERVAL_SECONDS =60, INTERVAL_LENGTH_MINUTES =5, MAX_STORAGE_SIZE_MB =256, QUERY_CAPTURE_MODE =ALL, SIZE_BASE_
 Une fois que nous aurons créé et rempli la base de données, et que nous aurons configuré Query Store, nous créerons une procédure stockée pour les tests :

UTILISEZ [CustomerDB] ; ALLEZ ANNULER LA PROCÉDURE SI EXISTE [dbo].[usp_GetCustomerInfo] ; ALLEZ CRÉER OU MODIFIER LA PROCÉDURE [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64))AS SELECT [CustomerID], [ FirstName], [LastName], [Email], CASE WHEN [Active] =1 THEN 'Active' ELSE 'Inactive' END [Status] FROM [dbo].[Customers] WHERE [LastName] =@LastName;

Remarque :j'ai utilisé la nouvelle syntaxe CREATE OR ALTER PROCEDURE qui est disponible dans le SP1.

Nous exécuterons notre procédure stockée plusieurs fois pour obtenir des données dans Query Store. J'ai ajouté WITH RECOMPILE parce que je sais que ces deux valeurs d'entrée généreront des plans différents, et je veux m'assurer de les capturer tous les deux.

EXEC [dbo].[usp_GetCustomerInfo] 'name' WITH RECOMPILE;GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' WITH RECOMPILE;

Si nous regardons dans Query Store, nous voyons la seule requête de notre procédure stockée et deux plans différents (chacun avec son propre plan_id). S'il s'agissait d'un environnement de production, nous aurions beaucoup plus de données en termes de statistiques d'exécution (durée, IO, informations CPU) et plus d'exécutions. Même si notre démo contient moins de données, la théorie est la même.

SELECT [qsq].[query_id], [qsp].[plan_id], [qsq].[object_id], [rs].[count_executions], DATEADD(MINUTE, -(DATEDIFF(MINUTE, GETDATE(), GETUTCDATE())), [qsp].[last_execution_time]) AS [LocalLastExecutionTime], [qst].[query_sql_text], ConvertedPlan =TRY_CONVERT(XML, [qsp].[query_plan])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'usp_GetCustomerInfo'); 

Requête Stocke les données de la requête de procédure stockée Requête Stocke les données après l'exécution de la procédure stockée (query_id =1) avec deux forfaits différents (plan_id =1, plan_id =2)

Plan de requête pour plan_id =1 (valeur d'entrée ='nom') Plan de requête pour plan_id =2 (valeur d'entrée ='query_cost')

Une fois que nous avons les informations dont nous avons besoin dans Query Store, nous pouvons cloner la base de données (les données Query Store seront incluses dans le clone par défaut) :

DBCC CLONEDATABASE (N'CustomerDB', N'CustomerDB_CLONE');

Comme je l'ai mentionné dans mon précédent article CLONEDATABASE, la base de données clonée est conçue pour être utilisée pour le support produit afin de tester les problèmes de performances des requêtes. En tant que tel, il est en lecture seule après avoir été cloné. Nous allons aller au-delà de ce que DBCC CLONEDATABASE est actuellement conçu pour faire, donc encore une fois, je veux juste vous rappeler cette note de la documentation Microsoft :

La base de données nouvellement générée générée à partir de DBCC CLONEDATABASE n'est pas prise en charge pour être utilisée comme base de données de production et est principalement destinée à des fins de dépannage et de diagnostic.

Afin d'apporter des modifications pour les tests, je dois sortir la base de données du mode lecture seule. Et je suis d'accord avec ça parce que je ne prévois pas de l'utiliser à des fins de production. Si cette base de données clonée se trouve dans un environnement de production, je vous recommande de la sauvegarder et de la restaurer sur un serveur de développement ou de test et d'y effectuer vos tests. Je ne recommande pas de tester en production, ni de tester contre l'instance de production (même avec une base de données différente).

/* Faites-le lire en écriture (sauvegardez-le et restaurez-le ailleurs pour ne pas travailler en production)*/ALTER DATABASE [CustomerDB_CLONE] SET READ_WRITE WITH NO_WAIT ;

Maintenant que je suis dans un état de lecture-écriture, je peux apporter des modifications, effectuer des tests et capturer des métriques. Je vais commencer par vérifier que j'obtiens le même plan qu'avant (rappel, vous ne verrez aucune sortie ici car il n'y a pas de données dans la base de données clonée) :

/* vérifie que nous obtenons le même forfait */USE [CustomerDB_CLONE];GOEXEC [dbo].[usp_GetCustomerInfo] 'name';GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' WITH RECOMPILE;

En vérifiant Query Store, vous verrez la même valeur plan_id qu'avant. Il existe plusieurs lignes pour la combinaison query_id/plan_id en raison des différents intervalles de temps pendant lesquels les données ont été capturées (déterminés par le paramètre INTERVAL_LENGTH_MINUTES, que nous avons défini sur 5).

SELECT [qsq].[query_id], [qsp].[plan_id], [qsq].[object_id], [rs].[count_executions], DATEADD(MINUTE, -(DATEDIFF(MINUTE, GETDATE(), GETUTCDATE())), [qsp].[last_execution_time]) AS [LocalLastExecutionTime], [rsi].[runtime_stats_interval_id], [rsi].[start_time], [rsi].[end_time], [qst].[query_sql_text] , ConvertedPlan =TRY_CONVERT(XML, [qsp].[query_plan])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]JOIN [sys].[query_store_runtime_stats_interval] [rsi] ON [rs].[runtime_stats_interval_id] =[rsi].[runtime_stats_interval_id]WHERE [qsq].[object_id] =OBJECT_ID(N'usp_GetCustomerInfo');GO

Interroger les données du magasin après l'exécution de la procédure stockée sur la base de données clonée

Tester les changements de code

Pour notre premier test, regardons comment nous pourrions tester une modification de notre code - plus précisément, nous modifierons notre procédure stockée pour supprimer la colonne [Active] de la liste SELECT.

/* Modifier la procédure à l'aide de CREATE OR ALTER (supprimer [Active] de la requête)*/CREATE OR ALTER PROCEDURE [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64))AS SELECT [CustomerID], [FirstName ], [LastName], [Email] FROM [dbo].[Clients] WHERE [LastName] =@LastName;

Réexécutez la procédure stockée :

EXEC [dbo].[usp_GetCustomerInfo] 'name' WITH RECOMPILE;GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' WITH RECOMPILE;

S'il vous arrivait d'afficher le plan d'exécution réel, vous remarquerez que les deux requêtes utilisent désormais le même plan, car la requête est couverte par l'index non clusterisé que nous avons créé à l'origine.

Plan d'exécution après modification de la procédure stockée pour supprimer [Active]

Nous pouvons vérifier avec Query Store que notre nouveau plan a un plan_id de 41 :

SELECT [qsq].[query_id], [qsp].[plan_id], [qsq].[object_id], [rs].[count_executions], DATEADD(MINUTE, -(DATEDIFF(MINUTE, GETDATE(), GETUTCDATE())), [qsp].[last_execution_time]) AS [LocalLastExecutionTime], [rsi].[runtime_stats_interval_id], [rsi].[start_time], [rsi].[end_time], [qst].[query_sql_text] , ConvertedPlan =TRY_CONVERT(XML, [qsp].[query_plan])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]JOIN [sys].[query_store_runtime_stats_interval] [rsi] ON [rs].[runtime_stats_interval_id] =[rsi].[runtime_stats_interval_id]WHERE [qsq].[object_id] =OBJECT_ID(N'usp_GetCustomerInfo');

Interroger les données du magasin après avoir modifié la procédure stockée

Vous remarquerez également ici qu'il y a un nouveau query_id (40). Query Store effectue une correspondance textuelle, et nous avons changé le texte de la requête, ainsi un nouveau query_id est généré. Notez également que l'object_id est resté le même, car use a utilisé la syntaxe CREATE OR ALTER. Faisons un autre changement, mais utilisez DROP puis CREATE OR ALTER.

/* Modifier la procédure en utilisant DROP puis CREATE OR ALTER (concaténer [FirstName] et [LastName])*/DROP PROCEDURE IF EXISTS [dbo].[usp_GetCustomerInfo];GO CREATE OR ALTER PROCEDURE [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64))AS SELECT [CustomerID], RTRIM([FirstName]) + ' ' + RTRIM([LastName]), [Email] FROM [dbo].[Customers] WHERE [LastName] =@ Nom ;

Maintenant, nous relançons la procédure :

EXEC [dbo].[usp_GetCustomerInfo] 'name';GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' WITH RECOMPILE ;

Maintenant, la sortie de Query Store devient plus intéressante, et notez que mon prédicat Query Store a changé en WHERE [qsq].[object_id] <> 0.

SELECT [qsq].[query_id], [qsp].[plan_id], [qsq].[object_id], [rs].[count_executions], DATEADD(MINUTE, -(DATEDIFF(MINUTE, GETDATE(), GETUTCDATE())), [qsp].[last_execution_time]) AS [LocalLastExecutionTime], [rsi].[runtime_stats_interval_id], [rsi].[start_time], [rsi].[end_time], [qst].[query_sql_text] , ConvertedPlan =TRY_CONVERT(XML, [qsp].[query_plan])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]JOIN [sys].[query_store_runtime_stats_interval] [rsi] ON [rs].[runtime_stats_interval_id] =[rsi].[runtime_stats_interval_id]WHERE [qsq].[object_id] <> 0 ;

Interroger les données du magasin après avoir modifié la procédure stockée à l'aide de DROP, puis de CREATE OR ALTER

L'object_id a changé en 661577395, et j'ai un nouveau query_id (42) car le texte de la requête a changé, et un nouveau plan_id (43). Bien que ce plan soit toujours une recherche d'index de mon index non clusterisé, il s'agit toujours d'un plan différent dans le magasin de requêtes. Comprenez que la méthode recommandée pour modifier des objets lorsque vous utilisez Query Store consiste à utiliser ALTER plutôt qu'un modèle DROP et CREATE. Cela est vrai en production et pour des tests comme celui-ci, car vous souhaitez conserver le même object_id pour faciliter la recherche de modifications.

Tester les modifications d'index

Pour la partie II de nos tests, plutôt que de modifier la requête, nous voulons voir si nous pouvons améliorer les performances en modifiant l'index. Nous allons donc remplacer la procédure stockée par la requête d'origine, puis modifier l'index.

CRÉER OU MODIFIER LA PROCÉDURE [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64))AS SELECT [CustomerID], [FirstName], [LastName], [Email], CASE WHEN [Active] =1 THEN 'Active' ELSE 'Inactive' END [Status] FROM [dbo].[Customers] WHERE [LastName] =@LastName;GO /* Modifier l'index existant pour ajouter [Active] pour couvrir la requête*/CREATE NONCLUSTERED INDEX [PhoneBook_Customers] ON [dbo].[Clients]([LastName],[FirstName])INCLUDE ([EMail], [Active])WITH (DROP_EXISTING=ON);

Étant donné que j'ai supprimé la procédure stockée d'origine, le plan d'origine n'est plus dans le cache. Si j'avais fait ce changement d'index en premier, dans le cadre des tests, rappelez-vous que la requête n'utiliserait pas automatiquement le nouvel index à moins que je ne force une recompilation. Je pourrais utiliser sp_recompile sur l'objet, ou je pourrais continuer à utiliser l'option WITH RECOMPILE sur la procédure pour voir que j'ai obtenu le même plan avec les deux valeurs différentes (rappelez-vous que j'avais initialement deux plans différents). Je n'ai pas besoin de WITH RECOMPILE car le plan n'est pas en cache, mais je le laisse activé par souci de cohérence.

EXEC [dbo].[usp_GetCustomerInfo] 'name' WITH RECOMPILE;GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' WITH RECOMPILE;

Dans Query Store, je vois un autre nouveau query_id (parce que l'object_id est différent de ce qu'il était à l'origine !) et un nouveau plan_id :

Interroger les données du magasin après l'ajout d'un nouvel index

Si je vérifie le plan, je peux voir que l'index modifié est utilisé.

Plan de requête après [Active] ajouté à l'index (plan_id =50)

Et maintenant que j'ai un plan différent, je pourrais aller plus loin et essayer de simuler une charge de travail de production pour vérifier qu'avec différents paramètres d'entrée, cette procédure stockée génère le même plan et utilise le nouvel index. Il y a une mise en garde ici, cependant. Vous avez peut-être remarqué l'avertissement sur l'opérateur Index Seek - cela se produit car il n'y a pas de statistiques sur la colonne [LastName]. Lorsque nous avons créé l'index avec [Active] comme colonne incluse, la table a été lue pour mettre à jour les statistiques. Il n'y a pas de données dans le tableau, d'où l'absence de statistiques. C'est certainement quelque chose à garder à l'esprit avec les tests d'index. Lorsque des statistiques sont manquantes, l'optimiseur utilise des heuristiques qui peuvent ou non convaincre l'optimiseur d'utiliser le plan que vous attendez.

Résumé

Je suis un grand fan de DBCC CLONEDATABASE. Je suis encore plus fan de Query Store. Lorsque vous associez les deux, vous disposez d'une grande capacité de test rapide des modifications d'index et de code. Avec cette méthode, vous examinez principalement les plans d'exécution pour valider les améliorations. Étant donné qu'il n'y a pas de données dans une base de données clonée, vous ne pouvez pas capturer l'utilisation des ressources et les statistiques d'exécution pour prouver ou réfuter un avantage perçu dans un plan d'exécution. Vous devez toujours restaurer la base de données et tester un ensemble complet de données - et Query Store peut toujours être d'une grande aide pour capturer des données quantitatives. Cependant, pour les cas où la validation du plan est suffisante, ou pour ceux d'entre vous qui n'effectuent aucun test actuellement, DBCC CLONEDATABASE fournit ce bouton simple que vous recherchiez. Query Store rend le processus encore plus simple.

Quelques éléments à noter :

Je ne recommande pas d'utiliser WITH RECOMPILE lors de l'appel de procédures stockées (ou de les déclarer de cette façon - voir le post de Paul White). J'ai utilisé cette option pour cette démo parce que j'ai créé une procédure stockée sensible aux paramètres et que je voulais m'assurer que les différentes valeurs généraient des plans différents et n'utilisaient pas de plan à partir du cache.

L'exécution de ces tests dans SQL Server 2014 SP2 avec DBCC CLONEDATABASE est tout à fait possible, mais il existe évidemment une approche différente pour capturer les requêtes et les métriques, ainsi que pour examiner les performances. Si vous souhaitez voir cette même méthodologie de test, sans Query Store, laissez un commentaire et faites-le moi savoir !