sql >> Database teknologi >  >> RDS >> Database

Brug af DBCC CLONEDATABASE og Query Store til test

Sidste sommer, efter at SP2 til SQL Server 2014 blev frigivet, skrev jeg om at bruge DBCC CLONEDATABASE til mere end blot at undersøge et problem med forespørgselsydeevne. En nylig kommentar til indlægget fra en læser fik mig til at tænke på, at jeg skulle uddybe, hvad jeg havde i tankerne om, hvordan man bruger den klonede database til test. Peter skrev:

"Jeg er primært en C#-udvikler, og mens jeg skriver og beskæftiger mig med T-SQL hele tiden, når det kommer til at gå ud over den SQL Server (stort set alle DBA-ting, statistik og lignende), ved jeg ikke rigtig meget . Ved ikke engang rigtig, hvordan jeg ville bruge en klon-DB som denne til justering af ydeevne"

Nå, Peter, her skal du. Jeg håber, at dette hjælper!

Opsætning

DBCC CLONEDATABASE blev gjort tilgængelig i SQL Server 2016 SP1, så det er det, vi vil bruge til test, da det er den aktuelle udgivelse, og fordi jeg kan bruge Query Store til at fange mine data. For at gøre livet lettere opretter jeg en database til test i stedet for at gendanne en prøve fra Microsoft.

BRUG [master];GO DROP DATABASE HVIS FINDER [KundeDB], [KundeDB_KLONE];GO /* Skift filplaceringer efter behov */ OPRET DATABASE [KundeDB] PÅ PRIMÆR (NAVN =N'KundeDB', FILNAVN =N' C:\Databaser\CustomerDB.mdf' , SIZE =512MB , MAXSIZE =UNLIMITED, FILEGROWTH =65536KB ) LOG PÅ ( NAME =N'CustomerDB_log', FILENAME =N'C:\Databases_log.SIZEDB5,\Customer' =_log.SIZEDB5,\Customer' MAKSSTØRRELSE =UBEGRÆNSET , FILVÆKST =65536KB );GO ALTER DATABASE [CustomerDB] INDSTIL GENDANNELSE ENKELT;

Opret nu en tabel og tilføj nogle data:

BRUG [KundeDB];GO OPRET TABEL [dbo].[Kunder]( [Kunde-ID] [int] IKKE NULL, [Fornavn] [nvarchar](64) IKKE NULL, [Efternavn] [nvarchar](64) IKKE NULL, [EMail] [nvarchar](320) IKKE NULL, [Aktiv] [bit] IKKE NULL STANDARD 1, [Oprettet] [datoklokkeslæt] IKKE NULL STANDARD SYSDATETIME(), [Opdateret] [datotidspunkt] NULL, BEGRÆNSNING [PK_Kunder] PRIMÆR NØGLE KLUSTERET ([KundeID]));GO /* Dette tilføjer 1.000.000 rækker til tabellen; du er velkommen til at tilføje mindre*/INSERT dbo.Kunder MED (TABLOCKX) (Kunde-ID, Fornavn, Efternavn, E-mail, [Aktiv]) VÆLG rn =ROW_NUMBER() OVER (ORDER BY n), fn, ln, em, a FROM ( VÆLG TOP (1000000) fn, ln, em, a =MAX(a), n =MAX(NEWID()) FRA ( VÆLG fn, ln, em, a, r =ROW_NUMBER() OVER (OPDELING EFTER em BESTIL EFTER em ) FRA ( VÆLG TOP (20000000) fn =VENSTRE(o.navn, 64), ln =VENSTRE(c.navn, 64), em =VENSTRE(o.navn, LEN(c.navn)%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 ORDRE AF NEWID() ) AS x ) AS y WHERE r =1 GRUPPER EFTER fn, ln, em BESTIL EFTER n ) AS z BESTIL AF rn;GO OPRET IKKE-KLUNGERET INDEX [Telefonbog_Kunder] PÅ [dbo].[Kunder]([Efternavn] ,[FirstName])INkluder ([E-mail]);

Nu aktiverer vi Query Store:

USE [master];GO ÆNDRING DATABASE [KundeDB] SET QUERY_STORE =TIL; Alter database [customerdb] sæt 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, forespørgsel 

Når vi har oprettet og udfyldt databasen, og vi har konfigureret Query Store, opretter vi en lagret procedure til test:

BRUG [CustomerDB];GO DROP PROCEDURE HVIS FINDER [dbo].[usp_GetCustomerInfo];GO OPRET ELLER ÆNDRING PROCEDURE [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64))AS SELECT], [CustomerID] Fornavn], [Efternavn], [E-mail], CASE WHEN [Aktiv] =1 SÅ 'Aktiv' ELLES 'Inaktiv' SLUT [Status] FRA [dbo].[Kunder] WHERE [Efternavn] =@Efternavn;

Bemærk:Jeg brugte den seje nye CREATE OR ALTER PROCEDURE syntaks, som er tilgængelig i SP1.

Vi kører vores lagrede procedure et par gange for at få nogle data i Query Store. Jeg har tilføjet MED RECOMPILE, fordi jeg ved, at disse to inputværdier vil generere forskellige planer, og jeg vil gerne sørge for at fange dem begge.

EXEC [dbo].[usp_GetCustomerInfo] 'navn' MED RECOMPILE; GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' MED RECOMPILE;

Hvis vi kigger i Query Store, ser vi den ene forespørgsel fra vores lagrede procedure og to forskellige planer (hver med sit eget plan_id). Hvis dette var et produktionsmiljø, ville vi have betydeligt flere data i form af runtime-statistik (varighed, IO, CPU-information) og flere eksekveringer. Selvom vores demo har færre data, er teorien den samme.

VÆLG [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])FRA [sys].[query_store_query] [ qsq] JOIN [sys].[query_store_query_text] [qst] TIL [qsq].[query_text_id] =[qst].[query_text_id]JOIN [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]HVOR [qsq].[object_id] =OBJECT_ID(N'usp_GetCustomerInfo'); 

Forespørgsel Gem data fra lagret procedureforespørgsel Query Store data efter udførelse af lagret procedure (query_id =1) med to forskellige planer (plan_id =1, plan_id =2)

Forespørgselsplan for plan_id =1 (inputværdi ='navn') Forespørgselsplan for plan_id =2 (inputværdi ='query_cost')

Når vi har de oplysninger, vi har brug for i Query Store, kan vi klone databasen (Query Store-data vil blive inkluderet i klonen som standard):

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

Som jeg nævnte i mit tidligere CLONEDATABASE-indlæg, er den klonede database designet til at blive brugt til produktsupport til at teste problemer med forespørgselsydeevne. Som sådan er den skrivebeskyttet, efter at den er klonet. Vi vil gå ud over, hvad DBCC CLONEDATABASE i øjeblikket er designet til at gøre, så igen, jeg vil bare minde dig om denne note fra Microsoft-dokumentationen:

Den nyligt genererede database, der er genereret fra DBCC CLONEDATABASE, understøttes ikke til brug som en produktionsdatabase og er primært beregnet til fejlfinding og diagnostiske formål.

For at foretage ændringer til test, skal jeg tage databasen ud af en skrivebeskyttet tilstand. Og jeg er ok med det, fordi jeg ikke planlægger at bruge dette til produktionsformål. Hvis denne klonede database er i et produktionsmiljø, anbefaler jeg, at du sikkerhedskopierer den og gendanner den på en dev- eller testserver og laver din test der. Jeg anbefaler ikke at teste i produktionen, og jeg anbefaler heller ikke at teste mod produktionsforekomsten (selv med en anden database).

/* Få det til at læse skriv (sikkerhedskopier det og gendan det et andet sted, så du ikke arbejder i produktionen)*/ALTER DATABASE [CustomerDB_CLONE] SET READ_WRITE WITH NO_WAIT;

Nu hvor jeg er i en læse-skrive-tilstand, kan jeg foretage ændringer, lave nogle test og indfange metrics. Jeg starter med at bekræfte, at jeg får den samme plan, som jeg gjorde før (påmindelse, du vil ikke se noget output her, fordi der ikke er nogen data i den klonede database):

/* bekræft, at vi får den samme plan */USE [CustomerDB_CLONE];GOEXEC [dbo].[usp_GetCustomerInfo] 'name';GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' MED REKOMPILERING;

Når du tjekker Query Store, vil du se den samme plan_id-værdi som før. Der er flere rækker for query_id/plan_id-kombinationen på grund af de forskellige tidsintervaller, som dataene blev registreret over (bestemt af INTERVAL_LENGTH_MINUTES-indstillingen, som vi satte til 5).

VÆLG [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])FRA [sys].[query_store_query] [qsq] JOIN [sys].[query_store_query_text] [qst] PÅ [qsq].[query_text_id] =[qst]. [query_text_id]JOIN [sys].[query_store_plan] [qsp] TIL [qsq].[query_id] =[qsp].[query_id]JOIN [sys].[query_store_runtime_stats] [rs] TIL [qsp].[plan_id] =[rs].[plan_id]JOIN [sys].[query_store_runtime_stats_interval] [rsi] PÅ [rs].[runtime_stats_interval_id] =[rsi].[runtime_stats_interval_id]HVOR [qsq].[object_id] =OBJECTtom_ID(N')ustom_ID(N');GO

Forespørg lagre data efter udførelse af den lagrede procedure mod den klonede database

Test kodeændringer

Til vores første test, lad os se på, hvordan vi kunne teste en ændring af vores kode – specifikt vil vi ændre vores lagrede procedure for at fjerne kolonnen [Aktiv] fra SELECT-listen.

/* Skift procedure ved hjælp af CREATE OR ALTER (fjern [Aktiv] fra forespørgsel)*/CREATE OR ALTER PROCEDURE [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64))SOM VÆLG [Kunde-ID], [FirstName ], [LastName], [Email] FRA [dbo].[Kunder] WHERE [LastName] =@LastName;

Kør den lagrede procedure igen:

EXEC [dbo].[usp_GetCustomerInfo] 'navn' MED RECOMPILE; GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' MED RECOMPILE;

Hvis du tilfældigvis viste den faktiske udførelsesplan, vil du bemærke, at begge forespørgsler nu bruger den samme plan, da forespørgslen er dækket af det ikke-klyngede indeks, vi oprindeligt oprettede.

Udførelsesplan efter ændring af lagret procedure for at fjerne [Active]

Vi kan bekræfte med Query Store, vores nye plan har et plan_id på 41:

VÆLG [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])FRA [sys].[query_store_query] [qsq] JOIN [sys].[query_store_query_text] [qst] PÅ [qsq].[query_text_id] =[qst]. [query_text_id]JOIN [sys].[query_store_plan] [qsp] TIL [qsq].[query_id] =[qsp].[query_id]JOIN [sys].[query_store_runtime_stats] [rs] TIL [qsp].[plan_id] =[rs].[plan_id]JOIN [sys].[query_store_runtime_stats_interval] [rsi] PÅ [rs].[runtime_stats_interval_id] =[rsi].[runtime_stats_interval_id]HVOR [qsq].[object_id] =OBJECTtom_ID(N')ustom_ID(N');

Forespørg på lagerdata efter ændring af den lagrede procedure

Du vil også bemærke her, at der er et nyt query_id (40). Query Store udfører tekstlig matchning, og vi har ændret teksten i forespørgslen, så der genereres et nyt query_id. Bemærk også, at object_id forblev det samme, fordi use brugte CREATE OR ALTER-syntaksen. Lad os lave en anden ændring, men brug DROP og derefter CREATE OR ALTER.

/* Skift procedure ved hjælp af DROP og derefter CREATE OR ALTER (sammenkæd [FirstName] og [LastName])*/DROP PROCEDURE HVIS FINDER [dbo].[usp_GetCustomerInfo];GO CREATE OR ALTER PROCEDURE [dbo].[usp_GetCustomer (@Efternavn [nvarchar](64))AS SELECT [Kunde-ID], RTRIM([Fornavn]) + ' ' + RTRIM([Efternavn]), [E-mail] FRA [dbo].[Kunder] HVOR [Efternavn] =@ Efternavn;

Nu kører vi proceduren igen:

EXEC [dbo].[usp_GetCustomerInfo] 'navn';GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' MED REKOMPILERING;

Nu bliver outputtet fra Query Store mere interessant, og bemærk, at mit Query Store-prædikat er ændret til WHERE [qsq].[object_id] <> 0.

VÆLG [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])FRA [sys].[query_store_query] [qsq] JOIN [sys].[query_store_query_text] [qst] PÅ [qsq].[query_text_id] =[qst]. [query_text_id]JOIN [sys].[query_store_plan] [qsp] TIL [qsq].[query_id] =[qsp].[query_id]JOIN [sys].[query_store_runtime_stats] [rs] TIL [qsp].[plan_id] =[rs].[plan_id]JOIN [sys].[query_store_runtime_stats_interval] [rsi] PÅ [rs].[runtime_stats_interval_id] =[rsi].[runtime_stats_interval_id]HVOR [qsq].[object_id] <> 0;

Forespørg lagre data efter ændring af den lagrede procedure ved hjælp af DROP og derefter CREATE OR ALTER

Object_id er ændret til 661577395, og jeg har et nyt query_id (42), fordi forespørgselsteksten er ændret, og et nyt plan_id (43). Selvom denne plan stadig er en indekssøgning af mit ikke-klyngede indeks, er det stadig en anden plan i Query Store. Forstå, at den anbefalede metode til at ændre objekter, når du bruger Query Store, er at bruge ALTER frem for et DROP og CREATE-mønster. Dette gælder i produktionen og for test som denne, da du ønsker at beholde object_id'et det samme for at gøre det lettere at finde ændringer.

Testindeksændringer

Til del II af vores test, i stedet for at ændre forespørgslen, ønsker vi at se, om vi kan forbedre ydeevnen ved at ændre indekset. Så vi vil ændre den lagrede procedure tilbage til den oprindelige forespørgsel og derefter ændre indekset.

OPRET ELLER ÆNDRING PROCEDURE [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64))SOM VÆLG [Kunde-ID], [FirstName], [LastName], [E-mail], CASE NÅR [Aktiv] =1 SÅ 'Aktiv' ANDET 'Inaktiv' SLUT [Status] FRA [dbo].[Kunder] WHERE [Efternavn] =@Efternavn;GO /* Rediger eksisterende indeks for at tilføje [Aktiv] til at dække forespørgslen*/OPRET IKKE-KLUSTERET INDEKS [Telefonbog_Kunder] PÅ [dbo].[Kunder]([Efternavn],[Fornavn])INKLUDERE ([E-mail], [Aktiv])Med (DROP_EXISTING=ON);

Fordi jeg droppede den oprindelige lagrede procedure, er den oprindelige plan ikke længere i cachen. Hvis jeg havde foretaget denne indeksændring først, som en del af testen, så husk, at forespørgslen ikke automatisk ville bruge det nye indeks, medmindre jeg fremtvang en omkompilering. Jeg kunne bruge sp_recompile på objektet, eller jeg kunne fortsætte med at bruge WITH RECOMPILE-indstillingen på proceduren for at se, at jeg fik den samme plan med de to forskellige værdier (husk, at jeg havde to forskellige planer i starten). Jeg har ikke brug for WITH RECOMPILE, da planen ikke er i cachen, men jeg lader den stå for konsekvensens skyld.

EXEC [dbo].[usp_GetCustomerInfo] 'navn' MED RECOMPILE; GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' MED RECOMPILE;

I Query Store ser jeg endnu et nyt query_id (fordi object_id er anderledes end det oprindeligt var!) og et nyt plan_id:

Forespørg butiksdata efter tilføjelse af nyt indeks

Hvis jeg tjekker planen, kan jeg se, at det ændrede indeks bliver brugt.

Forespørgselsplan efter [Active] føjet til indekset (plan_id =50)

Og nu hvor jeg har en anden plan, kunne jeg tage det et skridt videre og prøve at simulere en produktionsbelastning for at bekræfte, at med forskellige inputparametre genererer denne lagrede procedure den samme plan og bruger det nye indeks. Der er dog en advarsel her. Du har måske bemærket advarslen på indekssøgningsoperatøren - dette sker, fordi der ikke er nogen statistik i kolonnen [Efternavn]. Da vi oprettede indekset med [Aktiv] som en inkluderet kolonne, blev tabellen læst for at opdatere statistik. Der er ingen data i tabellen, derfor manglen på statistik. Dette er bestemt noget at huske på med indekstest. Når der mangler statistik, vil optimeringsværktøjet bruge heuristik, som måske eller måske ikke overbeviser optimeringsværktøjet til at bruge den plan, du forventer.

Oversigt

Jeg er stor fan af DBCC CLONEDATABASE. Jeg er en endnu større fan af Query Store. Når du sætter de to sammen, har du stor mulighed for hurtig test af indeks- og kodeændringer. Med denne metode ser du primært på eksekveringsplaner for at validere forbedringer. Fordi der ikke er data i en klonet database, kan du ikke fange ressourcebrug og runtime-statistik for at enten bevise eller modbevise en opfattet fordel i en eksekveringsplan. Du skal stadig gendanne databasen og teste mod et komplet sæt data – og Query Store kan stadig være en stor hjælp til at fange kvantitative data. Men i de tilfælde, hvor planvalideringen er tilstrækkelig, eller for dem af jer, der ikke laver nogen test i øjeblikket, giver DBCC CLONEDATABASE den nemme knap, du har ledt efter. Query Store gør processen endnu nemmere.

Et par ting at bemærke:

Jeg anbefaler ikke at bruge WITH RECOMPILE, når du kalder lagrede procedurer (eller erklærer dem på den måde - se Paul Whites indlæg). Jeg brugte denne mulighed til denne demo, fordi jeg oprettede en parameterfølsom lagret procedure, og jeg ville sikre mig, at de forskellige værdier genererede forskellige planer og ikke brugte en plan fra cache.

At køre disse tests i SQL Server 2014 SP2 med DBCC CLONEDATABASE er meget muligt, men der er åbenbart en anden tilgang til at fange forespørgsler og målinger samt se på ydeevne. Hvis du gerne vil se den samme testmetode uden Query Store, så skriv en kommentar og lad mig det vide!


  1. Opret fysiske sikkerhedskopier af dine MariaDB- eller MySQL-databaser

  2. Returner rækker i den nøjagtige rækkefølge, de blev indsat

  3. Hvordan håndterer man SQL-kolonnenavne, der ligner SQL-nøgleord?

  4. Nulstil root-adgangskoden til MySQL på Windows