Afgelopen zomer, nadat SP2 voor SQL Server 2014 was uitgebracht, schreef ik over het gebruik van DBCC CLONEDATABASE voor meer dan alleen het onderzoeken van een probleem met de prestatie van een query. Een recente opmerking van een lezer over het bericht zette me aan het denken dat ik moest uitweiden over wat ik in gedachten had over het gebruik van de gekloonde database voor het testen. Peter schreef:
"Ik ben voornamelijk een C#-ontwikkelaar en hoewel ik de hele tijd schrijf en met T-SQL werk als het gaat om verder te gaan dan die SQL Server (vrijwel alle DBA-dingen, statistieken en dergelijke), weet ik niet echt veel . Ik weet niet eens echt hoe ik een kloon-DB zoals deze zou gebruiken voor het afstemmen van prestaties"Nou Peter, hier ga je. Ik hoop dat dit helpt!
Instellen
DBCC CLONEDATABASE is beschikbaar gemaakt in SQL Server 2016 SP1, dus dat is wat we zullen gebruiken voor het testen omdat het de huidige release is, en omdat ik Query Store kan gebruiken om mijn gegevens vast te leggen. Om het leven gemakkelijker te maken, maak ik een database om te testen, in plaats van een voorbeeld van Microsoft te herstellen.
USE [master]; GO DROP DATABASE IF EXISTS [CustomerDB], [CustomerDB_CLONE]; GO /* Change file locations as appropriate */ 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;
Maak nu een tabel en voeg wat gegevens toe:
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 /* This adds 1,000,000 rows to the table; feel free to add 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) + '.' + 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 NONCLUSTERED INDEX [PhoneBook_Customers] ON [dbo].[Customers]([LastName],[FirstName]) INCLUDE ([EMail]);
Nu gaan we Query Store inschakelen:
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_BASED_CLEANUP_MODE = AUTO, MAX_PLANS_PER_QUERY = 200);
Zodra we de database hebben gemaakt en gevuld, en we Query Store hebben geconfigureerd, maken we een opgeslagen procedure voor het 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;
Let op:ik heb de coole nieuwe CREATE OR ALTER PROCEDURE-syntaxis gebruikt die beschikbaar is in SP1.
We zullen onze opgeslagen procedure een paar keer uitvoeren om wat gegevens in Query Store te krijgen. Ik heb toegevoegd WITH RECOMPILE omdat ik weet dat deze twee invoerwaarden verschillende plannen zullen genereren, en ik wil er zeker van zijn dat ik ze allebei vastleg.
EXEC [dbo].[usp_GetCustomerInfo] 'name' WITH RECOMPILE; GO EXEC [dbo].[usp_GetCustomerInfo] 'query_cost' WITH RECOMPILE;
Als we in Query Store kijken, zien we de ene query uit onze opgeslagen procedure en twee verschillende plannen (elk met zijn eigen plan_id). Als dit een productieomgeving was, zouden we aanzienlijk meer gegevens hebben in termen van runtime-statistieken (duur, IO, CPU-informatie) en meer uitvoeringen. Hoewel onze demo minder gegevens heeft, is de theorie hetzelfde.
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');
Query Gegevens van opgeslagen procedurequery opslaan Query Gegevens opslaan na uitvoering van opgeslagen procedure (query_id =1) met twee verschillende abonnementen (plan_id =1, plan_id =2)
Queryplan voor plan_id =1 (invoerwaarde ='naam') Queryplan voor plan_id =2 (invoerwaarde ='query_cost')Zodra we de informatie hebben die we nodig hebben in Query Store, kunnen we de database klonen (Query Store-gegevens worden standaard in de kloon opgenomen):
DBCC CLONEDATABASE (N'CustomerDB', N'CustomerDB_CLONE');
Zoals ik in mijn vorige CLONEDATABASE-bericht al zei, is de gekloonde database ontworpen om te worden gebruikt voor productondersteuning om problemen met de prestatie van query's te testen. Als zodanig is het alleen-lezen nadat het is gekloond. We gaan verder dan waar DBCC CLONEDATABASE momenteel voor is ontworpen, dus nogmaals, ik wil u herinneren aan deze opmerking uit de Microsoft-documentatie:
De nieuw gegenereerde database die is gegenereerd vanuit DBCC CLONEDATABASE wordt niet ondersteund voor gebruik als productiedatabase en is voornamelijk bedoeld voor probleemoplossing en diagnostische doeleinden.Om wijzigingen aan te brengen voor het testen, moet ik de database uit een alleen-lezen modus halen. En daar ben ik het mee eens, want ik ben niet van plan dit voor productiedoeleinden te gebruiken. Als deze gekloonde database zich in een productieomgeving bevindt, raad ik u aan er een back-up van te maken en deze te herstellen op een ontwikkel- of testserver en daar uw tests uit te voeren. Ik raad niet aan om in productie te testen, en ik raad ook niet aan om tegen te testen de productie-instantie (zelfs met een andere database).
/* Make it read write (back it up and restore it somewhere else so you're not working in production) */ ALTER DATABASE [CustomerDB_CLONE] SET READ_WRITE WITH NO_WAIT;
Nu ik me in een lees-schrijfstatus bevindt, kan ik wijzigingen aanbrengen, wat testen doen en statistieken vastleggen. Ik zal beginnen met te verifiëren dat ik hetzelfde plan krijg als voorheen (herinnering, je zult hier geen uitvoer zien omdat er geen gegevens in de gekloonde database zijn):
/* verify we get the same plan */ USE [CustomerDB_CLONE]; GO EXEC [dbo].[usp_GetCustomerInfo] 'name'; GO EXEC [dbo].[usp_GetCustomerInfo] 'query_cost' WITH RECOMPILE;
Bij het controleren van Query Store ziet u dezelfde plan_id-waarde als voorheen. Er zijn meerdere rijen voor de combinatie query_id/plan_id vanwege de verschillende tijdsintervallen waarin de gegevens zijn vastgelegd (bepaald door de instelling INTERVAL_LENGTH_MINUTES, die we hebben ingesteld op 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
Query Gegevens opslaan na het uitvoeren van de opgeslagen procedure op de gekloonde database
Wijzigingen testcode
Laten we voor onze eerste test eens kijken hoe we een wijziging in onze code kunnen testen - in het bijzonder zullen we onze opgeslagen procedure wijzigen om de kolom [Actief] uit de SELECT-lijst te verwijderen.
/* Change procedure using CREATE OR ALTER (remove [Active] from query) */ CREATE OR ALTER PROCEDURE [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64)) AS SELECT [CustomerID], [FirstName], [LastName], [Email] FROM [dbo].[Customers] WHERE [LastName] = @LastName;
Voer de opgeslagen procedure opnieuw uit:
EXEC [dbo].[usp_GetCustomerInfo] 'name' WITH RECOMPILE; GO EXEC [dbo].[usp_GetCustomerInfo] 'query_cost' WITH RECOMPILE;
Als u het daadwerkelijke uitvoeringsplan hebt weergegeven, zult u merken dat beide query's nu hetzelfde plan gebruiken, aangezien de query wordt gedekt door de niet-geclusterde index die we oorspronkelijk hebben gemaakt.
Uitvoeringsplan na wijziging van opgeslagen procedure om [Actief] te verwijderen
We kunnen verifiëren met Query Store, ons nieuwe abonnement heeft een plan_id van 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');
Query Gegevens opslaan na wijziging van de opgeslagen procedure
Je zult hier ook zien dat er een nieuwe query_id (40) is. Query Store voert tekstuele overeenkomsten uit en we hebben de tekst van de query gewijzigd, waardoor een nieuwe query_id wordt gegenereerd. Merk ook op dat de object_id hetzelfde is gebleven, omdat gebruik de CREATE OR ALTER-syntaxis heeft gebruikt. Laten we nog een wijziging aanbrengen, maar gebruik DROP en vervolgens CREATE OR ALTER.
/* Change procedure using DROP and then CREATE OR ALTER (concatenate [FirstName] and [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] = @LastName;
Nu voeren we de procedure opnieuw uit:
EXEC [dbo].[usp_GetCustomerInfo] 'name'; GO EXEC [dbo].[usp_GetCustomerInfo] 'query_cost' WITH RECOMPILE;
Nu wordt de uitvoer van Query Store interessanter en merk op dat mijn predikaat Query Store is gewijzigd in 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;
Query Gegevens opslaan na wijziging van de opgeslagen procedure met DROP en vervolgens CREATE OR ALTER
De object_id is gewijzigd in 661577395 en ik heb een nieuwe query_id (42) omdat de querytekst is gewijzigd, en een nieuwe plan_id (43). Hoewel dit plan nog steeds een indexzoektocht is van mijn niet-geclusterde index, is het nog steeds een ander plan in Query Store. Begrijp dat de aanbevolen methode voor het wijzigen van objecten wanneer u Query Store gebruikt, is om ALTER te gebruiken in plaats van een DROP- en CREATE-patroon. Dit geldt voor productie en voor tests zoals deze, omdat u de object_id hetzelfde wilt houden om het vinden van wijzigingen gemakkelijker te maken.
Testindexwijzigingen
Voor deel II van onze tests willen we, in plaats van de query te wijzigen, kijken of we de prestaties kunnen verbeteren door de index te wijzigen. We zullen de opgeslagen procedure dus terugzetten naar de oorspronkelijke zoekopdracht en vervolgens de index wijzigen.
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 /* Modify existing index to add [Active] to cover the query */ CREATE NONCLUSTERED INDEX [PhoneBook_Customers] ON [dbo].[Customers]([LastName],[FirstName]) INCLUDE ([EMail], [Active]) WITH (DROP_EXISTING=ON);
Omdat ik de oorspronkelijke opgeslagen procedure heb laten vallen, bevindt het oorspronkelijke plan zich niet langer in de cache. Als ik deze indexwijziging eerst had aangebracht, als onderdeel van het testen, onthoud dan dat de query niet automatisch de nieuwe index zou gebruiken, tenzij ik een hercompilatie forceerde. Ik zou sp_recompile op het object kunnen gebruiken, of ik zou de WITH RECOMPILE-optie voor de procedure kunnen blijven gebruiken om te zien dat ik hetzelfde plan kreeg met de twee verschillende waarden (onthoud dat ik aanvankelijk twee verschillende plannen had). Ik heb WITH RECOMPILE niet nodig omdat het plan niet in de cache zit, maar ik laat het aan staan omwille van de consistentie.
EXEC [dbo].[usp_GetCustomerInfo] 'name' WITH RECOMPILE; GO EXEC [dbo].[usp_GetCustomerInfo] 'query_cost' WITH RECOMPILE;
Binnen Query Store zie ik nog een nieuwe query_id (omdat de object_id anders is dan oorspronkelijk!) en een nieuwe plan_id:
Query Gegevens opslaan na toevoegen nieuwe index
Als ik het plan controleer, kan ik zien dat de gewijzigde index wordt gebruikt.
Queryplan na [Actief] toegevoegd aan de index (plan_id =50)
En nu ik een ander plan heb, kan ik nog een stap verder gaan en een productiewerklast proberen te simuleren om te verifiëren dat met verschillende invoerparameters deze opgeslagen procedure hetzelfde plan genereert en de nieuwe index gebruikt. Er is hier echter een voorbehoud. Je hebt misschien de waarschuwing op de Index Seek-operator opgemerkt - dit gebeurt omdat er geen statistieken zijn in de [LastName]-kolom. Toen we de index maakten met [Actief] als een opgenomen kolom, werd de tabel gelezen om de statistieken bij te werken. Er zijn geen gegevens in de tabel, vandaar het ontbreken van statistieken. Dit is zeker iets om in gedachten te houden bij indextesten. Wanneer statistieken ontbreken, gebruikt de optimizer heuristieken die de optimizer al dan niet kunnen overtuigen om het plan te gebruiken dat u verwacht.
Samenvatting
Ik ben een grote fan van DBCC CLONEDATABASE. Ik ben een nog grotere fan van Query Store. Als je deze twee samenvoegt, heb je geweldige mogelijkheden voor het snel testen van index- en codewijzigingen. Met deze methode kijk je vooral naar uitvoeringsplannen om verbeteringen te valideren. Omdat er geen gegevens in een gekloonde database zijn, kunt u geen brongebruik en runtime-statistieken vastleggen om een waargenomen voordeel in een uitvoeringsplan te bewijzen of te weerleggen. U moet de database nog steeds herstellen en testen met een volledige set gegevens - en Query Store kan nog steeds een enorme hulp zijn bij het vastleggen van kwantitatieve gegevens. Voor die gevallen waarin de validatie van het abonnement voldoende is, of voor degenen onder u die momenteel geen tests uitvoeren, biedt DBCC CLONEDATABASE die gemakkelijke knop waarnaar u op zoek was. Query Store maakt het proces nog eenvoudiger.
Een paar opmerkingen:
Ik raad niet aan om WITH RECOMPILE te gebruiken bij het aanroepen van opgeslagen procedures (of om ze op die manier te declareren - zie de post van Paul White). Ik heb deze optie voor deze demo gebruikt omdat ik een parametergevoelige opgeslagen procedure heb gemaakt en ik wilde ervoor zorgen dat de verschillende waarden verschillende plannen genereerden en geen plan uit de cache gebruikten.
Het uitvoeren van deze tests in SQL Server 2014 SP2 met DBCC CLONEDATABASE is heel goed mogelijk, maar er is natuurlijk een andere benadering voor het vastleggen van query's en metrische gegevens, evenals het kijken naar prestaties. Als je dezelfde testmethode wilt zien, zonder Query Store, laat dan een reactie achter en laat het me weten!