Förra sommaren, efter att SP2 för SQL Server 2014 släpptes, skrev jag om att använda DBCC CLONEDATABASE för mer än att bara undersöka ett frågeprestandaproblem. En nyligen kommentar till inlägget av en läsare fick mig att tänka att jag borde utöka vad jag hade i åtanke om hur man använder den klonade databasen för testning. Peter skrev:
"Jag är huvudsakligen en C#-utvecklare och medan jag skriver och hanterar T-SQL hela tiden när det kommer till att gå utöver den där SQL-servern (i stort sett alla DBA-grejer, statistik och liknande) vet jag inte så mycket. . Vet inte ens riktigt hur jag skulle använda en klon-DB som denna för prestandajustering"Tja Peter, varsågod. Jag hoppas att detta hjälper!
Inställningar
DBCC CLONEDATABASE gjordes tillgänglig i SQL Server 2016 SP1, så det är vad vi kommer att använda för att testa eftersom det är den aktuella versionen och eftersom jag kan använda Query Store för att fånga min data. För att göra livet enklare skapar jag en databas för testning, istället för att återställa ett prov från Microsoft.
ANVÄND [master];GO SLIP DATABAS OM FINNS [CustomerDB], [CustomerDB_CLONE];GO /* Ändra filplatser efter behov */ SKAPA DATABAS [CustomerDB] PÅ PRIMÄR (NAMN =N'CustomerDB', FILENAME =N' C:\Databaser\CustomerDB.mdf' , STORLEK =512MB , MAXSIZE =UNLIMITED, FILEGROWTH =65536KB ) LOGGA PÅ ( NAME =N'CustomerDB_log', FILENAME =N'C:\Databases_log.SIZEDB5,\Customer' =1,\CustomerDB_log' MAXSIZE =OBEGRÄNSAD , FILVÄXT =65536KB );GO ALTER DATABASE [CustomerDB] STÄLL IN ÅTERSTÄLLNING ENKEL;
Skapa nu en tabell och lägg till lite data:
ANVÄND [KundDB];GO SKAPA TABELL [dbo].[Kunder]( [Kund-ID] [int] NOT NULL, [FirstName] [nvarchar](64) NOT NULL, [Efternamn] [nvarchar](64) NOT NULL, [E-post] [nvarchar](320) NOT NULL, [Aktiv] [bit] NOT NULL DEFAULT 1, [Skapad] [datetime] NOT NULL DEFAULT SYSDATETIME(), [Uppdaterad] [datetime] NULL, CONSTRAINT [PK_Kunder] PRIMARY KEY CLUSTERED ([CustomerID]));GO /* Detta lägger till 1 000 000 rader i tabellen; lägg gärna till mindre*/INSERT dbo.Kunder MED (TABLOCKX) (Kund-ID, Förnamn, Efternamn, E-post, [Aktiv]) VÄLJ rn =ROW_NUMBER() ÖVER (ORDERA EFTER n), fn, ln, em, a FROM ( VÄLJ TOP (1000000) fn, ln, em, a =MAX(a), n =MAX(NEWID()) FRÅN ( VÄLJ fn, ln, em, a, r =ROW_NUMBER() ÖVER (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 SOM '%y%' THEN 0 ELSE 1 END FROM sys.all_objects AS o CROSS JOIN sys.all_columns AS c BESTÄLLNING BY NEWID() ) AS x ) AS y WHERE r =1 GROUP BY fn, ln, em ORDER BY n ) AS z ORDER BY rn;GO SKAPA ICKE CLUSTERED INDEX [PhoneBook_Customers] PÅ [dbo].[Customers]([LastName] ,[FirstName])INKLUDERA ([E-post]);
Nu kommer vi att aktivera Query Store:
ANVÄND [master];GO ÄNDRA DATABAS [KundDB] SET QUERY_STORE =PÅ; Alter databas [customerdb] set query_store (operation_mode =read_write, cleanup_policy =(stale_query_threshold_days =30), data_flush_interval_seconds =60, intervall_length_minutes =5, max_storage_size_mb =256, query_capture_mode_mode_mode_mode_mode_mode)När vi har skapat och fyllt i databasen, och vi har konfigurerat Query Store, skapar vi en lagrad procedur för testning:
ANVÄND [CustomerDB];GÅ SLÄPP PROCEDUR OM FINNS [dbo].[usp_GetCustomerInfo];GO SKAPA ELLER ÄNDRA PROCEDUR [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64))SOM VÄLJ], [CustomerID FirstName], [LastName], [Email], CASE WHEN [Active] =1 THEN 'Active' ANDERS 'Inactive' END [Status] FRÅN [dbo].[Kunder] WHERE [LastName] =@LastName;Notera:Jag använde den coola nya CREATE OR ALTER PROCEDURE-syntaxen som är tillgänglig i SP1.
Vi kommer att köra vår lagrade procedur ett par gånger för att få lite data i Query Store. Jag har lagt till MED RECOMPILE eftersom jag vet att dessa två indata kommer att generera olika planer, och jag vill se till att fånga dem båda.
EXEC [dbo].[usp_GetCustomerInfo] 'namn' MED RECOMPILE; GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' MED RECOMPILE;Om vi tittar i Query Store ser vi den ena frågan från vår lagrade procedur och två olika planer (var och en med sitt eget plan_id). Om detta vore en produktionsmiljö skulle vi ha betydligt mer data när det gäller körtidsstatistik (varaktighet, IO, CPU-information) och fler körningar. Även om vår demo har mindre data, är teorin densamma.
VÄLJ [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])FRÅN [sys].[query_store_query] [ qsq] JOIN [sys].[query_store_query_text] [qst] PÅ [qsq].[query_text_id] =[qst].[query_text_id]GÅ MED [sys].[query_store_plan] [qsp] PÅ [qsq].[query_id] =[ qsp].[query_id]GÅ MED [sys].[query_store_runtime_stats] [rs] PÅ [qsp].[plan_id] =[rs].[plan_id]VÄR [qsq].[object_id] =OBJECT_ID(N'usp_GetCustomerInfo');Fråga Lagra data från fråga om lagrad procedur Fråga Lagra data efter körning av lagrad procedur (query_id =1) med två olika planer (plan_id =1, plan_id =2)
Frågeplan för plan_id =1 (indatavärde ='namn') Frågeplan för plan_id =2 (indatavärde ='query_cost')När vi har den information vi behöver i Query Store kan vi klona databasen (Query Store-data kommer att inkluderas i klonen som standard):
DBCC CLONEDATABASE (N'CustomerDB', N'CustomerDB_CLONE');Som jag nämnde i mitt tidigare CLONEDATABASE-inlägg är den klonade databasen designad för att användas för produktsupport för att testa frågeprestandaproblem. Som sådan är den skrivskyddad efter att den har klonats. Vi kommer att gå längre än vad DBCC CLONEDATABASE för närvarande är utformad för att göra, så återigen, jag vill bara påminna dig om denna anteckning från Microsofts dokumentation:
Den nygenererade databasen som genereras från DBCC CLONEDATABASE stöds inte för att användas som en produktionsdatabas och är främst avsedd för felsökning och diagnostiska syften.För att göra några ändringar för testning måste jag ta databasen ur ett skrivskyddat läge. Och jag är ok med det eftersom jag inte planerar att använda detta för produktionsändamål. Om den här klonade databasen är i en produktionsmiljö rekommenderar jag att du säkerhetskopierar den och återställer den på en dev- eller testserver och gör dina tester där. Jag rekommenderar inte att testa i produktion och inte heller att testa emot produktionsinstansen (även med en annan databas).
/* Få den att läsa skriv (säkerhetskopiera den och återställa den någon annanstans så att du inte arbetar i produktionen)*/ALTER DATABASE [CustomerDB_CLONE] SET READ_WRITE WITH NO_WAIT;Nu när jag är i ett läs-skriv-tillstånd kan jag göra ändringar, göra några tester och fånga mätvärden. Jag börjar med att verifiera att jag får samma plan som jag gjorde tidigare (påminnelse, du kommer inte att se någon utdata här eftersom det inte finns några data i den klonade databasen):
/* verifiera att vi får samma plan */USE [CustomerDB_CLONE];GOEXEC [dbo].[usp_GetCustomerInfo] 'name';GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' MED RECOMPILE;När du kontrollerar Query Store ser du samma plan_id-värde som tidigare. Det finns flera rader för query_id/plan_id-kombinationen på grund av de olika tidsintervall som data samlades in (bestäms av inställningen INTERVAL_LENGTH_MINUTES, som vi satte till 5).
VÄLJ [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].[starttid], [rsi].[sluttid], [qst].[query_sql_text] , ConvertedPlan =TRY_CONVERT(XML, [qsp].[query_plan])FRÅN [sys].[query_store_query] [qsq] JOIN [sys].[query_store_query_text] [qst] PÅ [qsq].[query_text_id] =[qst]. [query_text_id]GÅ MED [sys].[query_store_plan] [qsp] PÅ [qsq].[query_id] =[qsp].[query_id]JOIN [sys].[query_store_runtime_stats] [rs] PÅ [qsp].[plan_id] =[rs].[plan_id]GÅ MED [sys].[query_store_runtime_stats_interval] [rsi] PÅ [rs].[runtime_stats_interval_id] =[rsi].[runtime_stats_interval_id]VÄR [qsq].[object_id] =OBJECTtom_ID(N')OJECTtom_ID(N');GOFråga lagra data efter att ha kört den lagrade proceduren mot den klonade databasen
Testa kodändringar
För vårt första test, låt oss titta på hur vi kan testa en ändring av vår kod – närmare bestämt kommer vi att modifiera vår lagrade procedur för att ta bort kolumnen [Active] från SELECT-listan.
/* Ändra procedur med CREATE OR ALTER (ta bort [Active] från frågan)*/CREATE OR ALTER PROCEDUR [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64))SOM VÄLJ [Kund-ID], [FirstName ], [LastName], [Email] FRÅN [dbo].[Kunder] WHERE [LastName] =@LastName;Kör den lagrade proceduren igen:
EXEC [dbo].[usp_GetCustomerInfo] 'namn' MED RECOMPILE; GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' MED RECOMPILE;Om du råkade visa den faktiska exekveringsplanen kommer du att märka att båda frågorna nu använder samma plan, eftersom frågan täcks av det icke-klustrade index som vi skapade ursprungligen.
Exekutivplan efter ändring av lagrad procedur för att ta bort [Active]
Vi kan verifiera med Query Store, vår nya plan har ett plan_id på 41:
VÄLJ [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].[starttid], [rsi].[sluttid], [qst].[query_sql_text] , ConvertedPlan =TRY_CONVERT(XML, [qsp].[query_plan])FRÅN [sys].[query_store_query] [qsq] JOIN [sys].[query_store_query_text] [qst] PÅ [qsq].[query_text_id] =[qst]. [query_text_id]GÅ MED [sys].[query_store_plan] [qsp] PÅ [qsq].[query_id] =[qsp].[query_id]JOIN [sys].[query_store_runtime_stats] [rs] PÅ [qsp].[plan_id] =[rs].[plan_id]GÅ MED [sys].[query_store_runtime_stats_interval] [rsi] PÅ [rs].[runtime_stats_interval_id] =[rsi].[runtime_stats_interval_id]VÄR [qsq].[object_id] =OBJECTtom_ID(N')OJECTtom_ID(N');Fråga lagra data efter att ha ändrat den lagrade proceduren
Du kommer också att märka här att det finns ett nytt query_id (40). Query Store utför textmatchning och vi ändrade texten i frågan, så ett nytt query_id genereras. Observera också att object_id förblev detsamma, eftersom use använde CREATE OR ALTER-syntaxen. Låt oss göra en annan förändring, men använd DROP och sedan CREATE OR ALTER.
/* Ändra procedur med DROP och sedan CREATE OR ALTER (sammanfoga [FirstName] och [LastName])*/DROP PROCEDUR OM FINNS [dbo].[usp_GetCustomerInfo]; GÅ SKAPA ELLER ÄNDRA PROCEDUR [dbo].[usp_GetCustomer (@LastName [nvarchar](64))SOM VÄLJ [Kund-ID], RTRIM([FirstName]) + ' ' + RTRIM([Efternamn]), [E-post] FRÅN [dbo].[Kunder] VAR [Efternamn] =@ Efternamn;Nu kör vi proceduren igen:
EXEC [dbo].[usp_GetCustomerInfo] 'name'; GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' MED RECOMPILE;Nu blir resultatet från Query Store mer intressant, och notera att mitt Query Store-predikat har ändrats till WHERE [qsq].[object_id] <> 0.
VÄLJ [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].[starttid], [rsi].[sluttid], [qst].[query_sql_text] , ConvertedPlan =TRY_CONVERT(XML, [qsp].[query_plan])FRÅN [sys].[query_store_query] [qsq] JOIN [sys].[query_store_query_text] [qst] PÅ [qsq].[query_text_id] =[qst]. [query_text_id]GÅ MED [sys].[query_store_plan] [qsp] PÅ [qsq].[query_id] =[qsp].[query_id]JOIN [sys].[query_store_runtime_stats] [rs] PÅ [qsp].[plan_id] =[rs].[plan_id]GÅ MED [sys].[query_store_runtime_stats_interval] [rsi] PÅ [rs].[runtime_stats_interval_id] =[rsi].[runtime_stats_interval_id]VÄR [qsq].[object_id] <> 0;Fråga Lagra data efter att ha ändrat den lagrade proceduren med DROP och sedan CREATE OR ALTER
Object_id har ändrats till 661577395, och jag har ett nytt query_id (42) eftersom frågetexten ändrats och ett nytt plan_id (43). Även om den här planen fortfarande är en indexsökning av mitt icke-klustrade index, är det fortfarande en annan plan i Query Store. Förstå att den rekommenderade metoden för att ändra objekt när du använder Query Store är att använda ALTER snarare än ett DROP och CREATE-mönster. Detta är sant i produktion och för tester som denna, eftersom du vill behålla object_id detsamma för att göra det lättare att hitta ändringar.
Testindexändringar
För del II av vår testning, snarare än att ändra frågan, vill vi se om vi kan förbättra prestandan genom att ändra indexet. Så vi kommer att ändra den lagrade proceduren tillbaka till den ursprungliga frågan och sedan ändra indexet.
SKAPA ELLER ÄNDRA PROCEDUR [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64))SOM VÄLJ [Kund-ID], [FirstName], [LastName], [E-post], CASE NÄR [Active] =1 DÅ 'Aktiv' ANNARS 'Inaktiv' END [Status] FRÅN [dbo].[Kunder] WHERE [Efternamn] =@Efternamn;GO /* Ändra befintligt index för att lägga till [Aktiv] för att täcka frågan*/SKAPA ICKELUSTERAT INDEX [Telefonbok_Kunder] PÅ [dbo].[Kunder]([Efternamn],[FirstName])INKLUDERA ([E-post], [Aktiv]) MED (DROP_EXISTING=ON);Eftersom jag tappade den ursprungliga lagrade proceduren finns inte längre den ursprungliga planen i cachen. Om jag hade gjort den här indexändringen först, som en del av testningen, kom ihåg att frågan inte automatiskt skulle använda det nya indexet om jag inte tvingade fram en omkompilering. Jag kan använda sp_recompile på objektet, eller så kan jag fortsätta att använda WITH RECOMPILE-alternativet på proceduren för att se att jag fick samma plan med de två olika värdena (kom ihåg att jag hade två olika planer från början). Jag behöver inte MED RECOMPILE eftersom planen inte finns i cachen, men jag lämnar den på för konsekvensens skull.
EXEC [dbo].[usp_GetCustomerInfo] 'namn' MED RECOMPILE; GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' MED RECOMPILE;I Query Store ser jag ytterligare ett nytt query_id (eftersom object_id är annorlunda än det var ursprungligen!) och ett nytt plan_id:
Fråga Store-data efter att ha lagt till nytt index
Om jag kontrollerar planen kan jag se att det modifierade indexet används.
Frågeplan efter att [Active] lagts till i indexet (plan_id =50)
Och nu när jag har en annan plan, skulle jag kunna ta det ett steg längre och försöka simulera en produktionsbelastning för att verifiera att med olika ingångsparametrar genererar denna lagrade procedur samma plan och använder det nya indexet. Det finns dock en varning här. Du kanske har märkt varningen på Index Seek-operatorn – detta inträffar eftersom det inte finns någon statistik i kolumnen [Efternamn]. När vi skapade indexet med [Active] som en inkluderad kolumn, lästes tabellen för att uppdatera statistik. Det finns inga uppgifter i tabellen, därav bristen på statistik. Detta är definitivt något att tänka på vid indextestning. När statistik saknas kommer optimeraren att använda heuristik som kan eller kanske inte övertygar optimeraren att använda planen du förväntar dig.
Sammanfattning
Jag är ett stort fan av DBCC CLONEDATABASE. Jag är ett ännu större fan av Query Store. När du sätter ihop de två har du stor förmåga att snabbt testa index- och kodändringar. Med den här metoden tittar du främst på genomförandeplaner för att validera förbättringar. Eftersom det inte finns några data i en klonad databas kan du inte fånga resursanvändning och körtidsstatistik för att antingen bevisa eller motbevisa en upplevd fördel i en exekveringsplan. Du behöver fortfarande återställa databasen och testa mot en fullständig uppsättning data – och Query Store kan fortfarande vara till stor hjälp för att fånga in kvantitativ data. Men för de fall där planvalideringen är tillräcklig, eller för de av er som inte gör några tester för närvarande, tillhandahåller DBCC CLONEDATABASE den enkla knappen du har letat efter. Query Store gör processen ännu enklare.
Några anmärkningsvärda:
Jag rekommenderar inte att du använder WITH RECOMPILE när du anropar lagrade procedurer (eller deklarerar dem på det sättet – se Paul Whites inlägg). Jag använde det här alternativet för den här demon eftersom jag skapade en parameterkänslig lagrad procedur och jag ville se till att de olika värdena genererade olika planer och inte använde en plan från cache.
Att köra dessa tester i SQL Server 2014 SP2 med DBCC CLONEDATABASE är fullt möjligt, men det finns uppenbarligen ett annat tillvägagångssätt för att fånga frågor och mätvärden, samt titta på prestanda. Om du vill se samma testmetod, utan Query Store, lämna en kommentar och låt mig veta!