Database
 sql >> Datenbank >  >> RDS >> Database

Verwenden von DBCC CLONEDATABASE und Abfragespeicher zum Testen

Letzten Sommer, nachdem SP2 für SQL Server 2014 veröffentlicht wurde, schrieb ich über die Verwendung von DBCC CLONEDATABASE für mehr als nur die Untersuchung eines Problems mit der Abfrageleistung. Ein kürzlich erschienener Kommentar eines Lesers zu dem Beitrag brachte mich auf den Gedanken, dass ich meine Gedanken zur Verwendung der geklonten Datenbank zum Testen erweitern sollte. Peter hat geschrieben:

„Ich bin hauptsächlich ein C#-Entwickler und während ich die ganze Zeit über T-SQL schreibe und mich mit T-SQL beschäftige, wenn es darum geht, über diesen SQL Server hinauszugehen (so ziemlich alles DBA-Zeug, Statistiken und dergleichen), weiß ich nicht wirklich viel . Ich weiß nicht einmal wirklich, wie ich eine Klon-DB wie diese zur Leistungsoptimierung verwenden würde.

Nun Peter, bitte schön. Ich hoffe, das hilft!

Einrichtung

DBCC CLONEDATABASE wurde in SQL Server 2016 SP1 verfügbar gemacht, also werden wir das zum Testen verwenden, da es die aktuelle Version ist und weil ich den Abfragespeicher verwenden kann, um meine Daten zu erfassen. Um das Leben einfacher zu machen, erstelle ich eine Datenbank zum Testen, anstatt ein Beispiel von Microsoft wiederherzustellen.

USE [master];GO DROP DATABASE IF EXISTS [CustomerDB], [CustomerDB_CLONE];GO /* Dateispeicherorte nach Bedarf ändern */ CREATE DATABASE [CustomerDB] ON PRIMARY ( NAME =N'CustomerDB', FILENAME =N' C:\Databases\CustomerDB.mdf' , SIZE =512MB , MAXSIZE =UNLIMITED, FILEGROWTH =65536KB ) ANMELDEN ( NAME =N'CustomerDB_log', FILENAME =N'C:\Databases\CustomerDB_log.ldf' , SIZE =512MB , MAXSIZE =UNLIMITED , FILEGROWTH =65536KB );GO ALTER DATABASE [CustomerDB] SET RECOVERY SIMPLE;

Erstellen Sie nun eine Tabelle und fügen Sie einige Daten hinzu:

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, [Created] [datetime] NOT NULL DEFAULT SYSDATETIME(), [Updated] [datetime] NULL, CONSTRAINT [PK_Customers] PRIMARY KEY CLUSTERED ([CustomerID]));GO /* Dies fügt der Tabelle 1.000.000 Zeilen hinzu; Fühlen Sie sich frei, weniger hinzuzufügen*/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) + '.' + LEFT(c.name, LEN(o.name)%5+2) + '@' + RIGHT(c.name, LEN(o.name + c.name)%12 + 1) + LEFT( 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 UNCLUSTERED INDEX [PhoneBook_Customers] ON [dbo].[Customers]([LastName] ,[Vorname])INCLUDE ([EMail]);

Jetzt aktivieren wir den Abfragespeicher:

USE [master];GO ALTER DATABASE [KundenDB] SET QUERY_STORE =ON; Änderung der Datenbank [CustomerDB] Setzen Sie 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_mod_based_based_mb =256. 

Nachdem wir die Datenbank erstellt und gefüllt und den Abfragespeicher konfiguriert haben, erstellen wir eine gespeicherte Prozedur zum Testen:

USE [CustomerDB];GO DROP PROCEDURE IF EXISTS [dbo].[usp_GetCustomerInfo];GO CREATE OR ALTER PROCEDURE [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;

Beachten Sie:Ich habe die coole neue CREATE OR ALTER PROCEDURE-Syntax verwendet, die in SP1 verfügbar ist.

Wir werden unsere gespeicherte Prozedur ein paar Mal ausführen, um einige Daten im Abfragespeicher abzurufen. Ich habe WITH RECOMPILE hinzugefügt, weil ich weiß, dass diese beiden Eingabewerte unterschiedliche Pläne generieren und ich sicherstellen möchte, dass sie beide erfasst werden.

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

Wenn wir im Abfragespeicher nachsehen, sehen wir die eine Abfrage aus unserer gespeicherten Prozedur und zwei verschiedene Pläne (jeder mit seiner eigenen plan_id). Wenn dies eine Produktionsumgebung wäre, hätten wir deutlich mehr Daten in Bezug auf Laufzeitstatistiken (Dauer, IO, CPU-Informationen) und mehr Ausführungen. Auch wenn unsere Demo weniger Daten enthält, ist die Theorie dieselbe.

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'); 

Speicherdaten aus der gespeicherten Prozedur abfragen Speicherdaten nach der Ausführung der gespeicherten Prozedur abfragen (query_id =1) mit zwei verschiedenen Plänen (plan_id =1, plan_id =2)

Plan abfragen für plan_id =1 (Eingabewert ='name') Plan abfragen für plan_id =2 (Eingabewert ='query_cost')

Sobald wir die benötigten Informationen im Abfragespeicher haben, können wir die Datenbank klonen (die Abfragespeicherdaten werden standardmäßig in den Klon eingeschlossen):

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

Wie ich in meinem vorherigen Beitrag zu CLONEDATABASE erwähnt habe, wurde die geklonte Datenbank für den Produktsupport entwickelt, um Probleme mit der Abfrageleistung zu testen. Daher ist es nach dem Klonen schreibgeschützt. Wir werden über das hinausgehen, wofür DBCC CLONEDATABASE derzeit entwickelt wurde, also möchte ich Sie noch einmal an diesen Hinweis aus der Microsoft-Dokumentation erinnern:

Die neu generierte Datenbank, die von DBCC CLONEDATABASE generiert wird, wird nicht für die Verwendung als Produktionsdatenbank unterstützt und ist in erster Linie für Problembehandlungs- und Diagnosezwecke vorgesehen.

Um Änderungen zum Testen vorzunehmen, muss ich die Datenbank aus einem schreibgeschützten Modus nehmen. Und ich bin damit einverstanden, weil ich nicht vorhabe, dies für Produktionszwecke zu verwenden. Wenn sich diese geklonte Datenbank in einer Produktionsumgebung befindet, empfehle ich Ihnen, sie zu sichern und auf einem Entwicklungs- oder Testserver wiederherzustellen und dort Ihre Tests durchzuführen. Ich empfehle weder das Testen in der Produktion noch das Testen gegen die Produktionsinstanz (auch mit einer anderen Datenbank).

/* Machen Sie es schreibgeschützt (sichern Sie es und stellen Sie es woanders wieder her, damit Sie nicht in der Produktion arbeiten)*/ALTER DATABASE [CustomerDB_CLONE] SET READ_WRITE WITH NO_WAIT;

Jetzt, da ich mich in einem Lese-/Schreibmodus befinde, kann ich Änderungen vornehmen, einige Tests durchführen und Metriken erfassen. Ich beginne damit, zu überprüfen, ob ich denselben Plan wie zuvor bekomme (Erinnerung, Sie werden hier keine Ausgabe sehen, weil es keine Daten in der geklonten Datenbank gibt):

/* vergewissern Sie sich, dass wir denselben Plan erhalten */USE [CustomerDB_CLONE];GOEXEC [dbo].[usp_GetCustomerInfo] 'name';GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' WITH RECOMPILE;

Beim Überprüfen des Abfragespeichers sehen Sie den gleichen plan_id-Wert wie zuvor. Es gibt mehrere Zeilen für die Kombination query_id/plan_id aufgrund der unterschiedlichen Zeitintervalle, in denen die Daten erfasst wurden (bestimmt durch die Einstellung INTERVAL_LENGTH_MINUTES, die wir auf 5 setzen).

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

Speicherdaten abfragen, nachdem die gespeicherte Prozedur für die geklonte Datenbank ausgeführt wurde

Testen von Codeänderungen

Sehen wir uns für unseren ersten Test an, wie wir eine Änderung an unserem Code testen können – insbesondere ändern wir unsere gespeicherte Prozedur, um die Spalte [Active] aus der SELECT-Liste zu entfernen.

/* Verfahren mit CREATE OR ALTER ändern ([Active] aus Abfrage entfernen)*/CREATE OR ALTER PROCEDURE [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64))AS SELECT [CustomerID], [FirstName ], [Nachname], [E-Mail] FROM [dbo].[Kunden] WHERE [Nachname] =@Nachname;

Führen Sie die gespeicherte Prozedur erneut aus:

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

Wenn Sie zufällig den tatsächlichen Ausführungsplan angezeigt haben, werden Sie feststellen, dass beide Abfragen jetzt denselben Plan verwenden, da die Abfrage durch den ursprünglich erstellten Nonclustered-Index abgedeckt ist.

Ausführungsplan nach Änderung der gespeicherten Prozedur zum Entfernen von [Active]

Wir können mit Query Store überprüfen, dass unser neuer Plan eine plan_id von 41 hat:

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');

Speicherdaten nach Änderung der gespeicherten Prozedur abfragen

Sie werden hier auch feststellen, dass es eine neue query_id (40) gibt. Der Abfragespeicher führt einen Textabgleich durch, und wir haben den Text der Abfrage geändert, sodass eine neue query_id generiert wird. Beachten Sie auch, dass die object_id gleich geblieben ist, da use die CREATE OR ALTER-Syntax verwendet hat. Lassen Sie uns eine weitere Änderung vornehmen, aber verwenden Sie DROP und dann CREATE OR ALTER.

/* Verfahren mit DROP ändern und dann CREATE OR ALTER ([Vorname] und [Nachname] verketten)*/DROP PROCEDURE IF EXISTS [dbo].[usp_GetCustomerInfo];GO CREATE OR ALTER PROCEDURE [dbo].[usp_GetCustomerInfo] (@Nachname [nvarchar](64))AS SELECT [Kunden-ID], RTRIM([Vorname]) + ' ' + RTRIM([Nachname]), [E-Mail] FROM [dbo].[Kunden] WHERE [Nachname] =@ Nachname;

Jetzt führen wir die Prozedur erneut aus:

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

Jetzt wird die Ausgabe des Abfragespeichers interessanter, und beachten Sie, dass sich mein Abfragespeicher-Prädikat in WHERE [qsq].[object_id] <> 0.

geändert hat
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;

Speicherdaten nach dem Ändern der gespeicherten Prozedur mit DROP und dann CREATE OR ALTER abfragen

Die object_id hat sich in 661577395 geändert, und ich habe eine neue query_id (42), weil sich der Abfragetext geändert hat, und eine neue plan_id (43). Während dieser Plan immer noch eine Indexsuche meines nicht gruppierten Index ist, ist es immer noch ein anderer Plan im Abfragespeicher. Beachten Sie, dass die empfohlene Methode zum Ändern von Objekten bei Verwendung des Abfragespeichers darin besteht, ALTER anstelle eines DROP- und CREATE-Musters zu verwenden. Dies gilt in der Produktion und für solche Tests, da Sie die object_id beibehalten möchten, um das Auffinden von Änderungen zu erleichtern.

Indexänderungen testen

Für Teil II unseres Tests möchten wir, anstatt die Abfrage zu ändern, sehen, ob wir die Leistung verbessern können, indem wir den Index ändern. Also ändern wir die gespeicherte Prozedur zurück zur ursprünglichen Abfrage und ändern dann den Index.

CREATE OR ALTER PROCEDURE [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 /* Vorhandenen Index ändern, um [Active] hinzuzufügen, um die Abfrage abzudecken*/CREATE NONCLUSTERED INDEX [PhoneBook_Customers] ON [dbo].[Customers]([LastName],[FirstName])INCLUDE ([EMail], [Active])WITH (DROP_EXISTING=ON);

Da ich die ursprüngliche gespeicherte Prozedur gelöscht habe, befindet sich der ursprüngliche Plan nicht mehr im Cache. Wenn ich diese Indexänderung zuerst als Teil des Testens vorgenommen hätte, denken Sie daran, dass die Abfrage den neuen Index nicht automatisch verwenden würde, es sei denn, ich hätte eine Neukompilierung erzwungen. Ich könnte sp_recompile für das Objekt verwenden, oder ich könnte weiterhin die Option WITH RECOMPILE für die Prozedur verwenden, um zu sehen, dass ich denselben Plan mit zwei unterschiedlichen Werten erhalten habe (denken Sie daran, dass ich ursprünglich zwei verschiedene Pläne hatte). Ich brauche WITH RECOMPILE nicht, da sich der Plan nicht im Cache befindet, aber ich lasse ihn aus Gründen der Konsistenz eingeschaltet.

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

Im Query Store sehe ich eine weitere neue query_id (weil die object_id anders ist als ursprünglich!) und eine neue plan_id:

Speicherdaten nach Hinzufügen eines neuen Index abfragen

Wenn ich den Plan überprüfe, kann ich sehen, dass der geänderte Index verwendet wird.

Plan abfragen, nachdem [Active] zum Index hinzugefügt wurde (plan_id =50)

Und jetzt, da ich einen anderen Plan habe, könnte ich noch einen Schritt weiter gehen und versuchen, eine Produktionsworkload zu simulieren, um zu überprüfen, ob diese gespeicherte Prozedur mit anderen Eingabeparametern denselben Plan generiert und den neuen Index verwendet. Hier gibt es jedoch eine Einschränkung. Möglicherweise ist Ihnen die Warnung zum Index Seek-Operator aufgefallen – dies tritt auf, weil es keine Statistiken für die Spalte [Nachname] gibt. Als wir den Index mit [Active] als eingeschlossener Spalte erstellt haben, wurde die Tabelle gelesen, um Statistiken zu aktualisieren. Es gibt keine Daten in der Tabelle, daher das Fehlen von Statistiken. Dies ist definitiv etwas, das man beim Index-Testen im Hinterkopf behalten sollte. Wenn Statistiken fehlen, verwendet der Optimierer Heuristiken, die den Optimierer davon überzeugen können oder nicht, den erwarteten Plan zu verwenden.

Zusammenfassung

Ich bin ein großer Fan von DBCC CLONEDATABASE. Ich bin ein noch größerer Fan von Query Store. Wenn Sie die beiden zusammenfügen, haben Sie eine hervorragende Möglichkeit, Index- und Codeänderungen schnell zu testen. Bei dieser Methode sehen Sie sich in erster Linie Ausführungspläne an, um Verbesserungen zu validieren. Da in einer geklonten Datenbank keine Daten vorhanden sind, können Sie keine Ressourcennutzungs- und Laufzeitstatistiken erfassen, um einen wahrgenommenen Vorteil in einem Ausführungsplan zu beweisen oder zu widerlegen. Sie müssen immer noch die Datenbank wiederherstellen und mit einem vollständigen Datensatz testen – und Query Store kann immer noch eine große Hilfe bei der Erfassung quantitativer Daten sein. Für die Fälle, in denen die Planvalidierung ausreicht, oder für diejenigen unter Ihnen, die derzeit keine Tests durchführen, bietet DBCC CLONEDATABASE die einfache Schaltfläche, nach der Sie gesucht haben. Query Store macht den Prozess noch einfacher.

Einige Anmerkungen:

Ich empfehle nicht, WITH RECOMPILE beim Aufrufen gespeicherter Prozeduren zu verwenden (oder sie so zu deklarieren – siehe Beitrag von Paul White). Ich habe diese Option für diese Demo verwendet, weil ich eine parameterabhängige gespeicherte Prozedur erstellt habe und sicherstellen wollte, dass die unterschiedlichen Werte unterschiedliche Pläne generieren und keinen Plan aus dem Cache verwenden.

Das Ausführen dieser Tests in SQL Server 2014 SP2 mit DBCC CLONEDATABASE ist durchaus möglich, aber es gibt offensichtlich einen anderen Ansatz zum Erfassen von Abfragen und Metriken sowie zum Betrachten der Leistung. Wenn Sie dieselbe Testmethodik ohne Query Store sehen möchten, hinterlassen Sie einen Kommentar und lassen Sie es mich wissen!