Database
 sql >> Baza danych >  >> RDS >> Database

Używanie DBCC CLONEDATABASE i Query Store do testowania

Zeszłego lata, po wydaniu dodatku SP2 dla SQL Server 2014, pisałem o używaniu DBCC CLONEDATABASE do czegoś więcej niż tylko zbadania problemu z wydajnością zapytań. Niedawny komentarz czytelnika pod postem sprawił, że pomyślałem, że powinienem rozwinąć to, co miałem na myśli, jak używać sklonowanej bazy danych do testów. Piotr napisał:

„Jestem głównie programistą C# i chociaż cały czas piszę i zajmuję się T-SQL, jeśli chodzi o wyjście poza ten SQL Server (prawie wszystkie rzeczy DBA, statystyki i tym podobne), tak naprawdę niewiele wiem . Nawet nie wiem, w jaki sposób mógłbym użyć klonowanej bazy danych, takiej jak ta, do dostrajania wydajności”

Cóż Peter, proszę bardzo. Mam nadzieję, że to pomoże!

Konfiguracja

DBCC CLONEDATABASE został udostępniony w SQL Server 2016 SP1, więc tego użyjemy do testowania, ponieważ jest to bieżąca wersja, i ponieważ mogę używać Query Store do przechwytywania moich danych. Aby ułatwić życie, tworzę bazę danych do testów, zamiast przywracać próbkę z Microsoftu.

USE [master];GO DROP DATABASE IF EXISTS [CustomerDB], [CustomerDB_CLONE];GO /* Zmień odpowiednio lokalizacje plików */ CREATE DATABASE [CustomerDB] ON PRIMARY ( NAME =N'CustomerDB', FILENAME =N' C:\Databases\CustomerDB.mdf' , SIZE =512MB , MAXSIZE =UNLIMITED, FILEGROWTH =65536KB ) ZALOGUJ ( NAZWA =N'CustomerDB_log', FILENAME =N'C:\Databases\CustomerDB_log.ldf' , SIZE =512MB , MAXSIZE =UNLIMITED , FILEGROWTH =65536KB );GO ALTER DATABASE [CustomerDB] SET RECOVERY PROSTE;

Teraz utwórz tabelę i dodaj trochę danych:

 USE [CustomerDB];GO CREATE TABLE [dbo].[Klienci]( [CustomerID] [int] NOT NULL, [Imię] [nvarchar](64) NOT NULL, [Nazwisko] [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 /* Dodaje 1 000 000 wierszy do tabeli; możesz dodać mniej*/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) + '.' + LEWY(c.nazwa, LEN(o.nazwa)%5+2) + '@' + PRAWY(c.nazwa, LEN(o.nazwa + c.nazwa)%12 + 1) + LEWY( RTRIM(CHECKSUM(NEWID())),3) + '.com', a =CASE GDY c.name LIKE '%y%' TO 0 JESZCZE 1 KONIEC Z sys.all_objects AS o CROSS DOŁĄCZ sys.all_columns JAK C ORDER BY NEWID() ) AS x ) AS y GDZIE r =1 GRUPA WG fn, ln, em ZAMÓW WG n ) AS z ZAMÓW WEDŁUG rn;GO UTWÓRZ INDEKS NIESKLASTRAROWANY [Klienci_książki_tel.] NA [dbo].[Klienci]([Nazwisko] ,[Imię]) INCLUDE ([e-mail]);

Teraz włączymy Query Store:

 USE [master];GO ALTER DATABASE [CustomerDB] SET QUERY_STORE =ON; ALTER DATABASE [CustomerDB] SET QUERY_STORE (OPERATION_MODE =ODCZYT_ZAPISU, CLEANUP_POLICY =(STALE_QUERY_THRESHOLD_DAYS =30), DATA_FLUSH_INTERVAL_SECONDS =60, INTERVAL_LENGTH_MINUTES =5, MAX_STORAGE_STORAGE_SIZE_DE_MO> =200_PL_PYT. 

Po utworzeniu i wypełnieniu bazy danych oraz skonfigurowaniu magazynu zapytań utworzymy procedurę składowaną do testowania:

UŻYJ [CustomerDB];GO DROP PROCEDURE, JEŚLI ISTNIEJE [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].[Klienci] WHERE [LastName] =@LastName;

Uwaga:użyłem nowej, fajnej składni CREATE OR ALTER PROCEDURE, która jest dostępna w SP1.

Kilka razy uruchomimy naszą procedurę składowaną, aby pobrać dane z magazynu zapytań. Dodałem WITH RECOMPILE, ponieważ wiem, że te dwie wartości wejściowe wygenerują różne plany i chcę mieć pewność, że przechwycisz je oba.

EXEC [dbo].[usp_GetCustomerInfo] 'nazwa' Z RECOMPILE;GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' Z RECOMPILE;

Jeśli spojrzymy w Query Store, zobaczymy jedno zapytanie z naszej procedury składowanej i dwa różne plany (każdy z własnym plan_id). Gdyby to było środowisko produkcyjne, mielibyśmy znacznie więcej danych pod względem statystyk czasu wykonywania (czas trwania, IO, informacje o procesorze) i więcej wykonań. Mimo że nasze demo zawiera mniej danych, teoria jest taka sama.

SELECT [qsq].[query_id], [qsp].[plan_id], [qsq].[object_id], [rs].[count_executions], DATEADD(MINUTE, -(DATEDIFF(MINUTE, GETDATE(), GETUTCDATE())), [qsp].[czas_ostatniego_wykonania]) AS [LocalLastExecutionTime], [qst].[query_sql_text], ConvertedPlan =TRY_CONVERT(XML, [qsp].[query_plan])FROM [sys].[query_store_query] [ qsq] DOŁĄCZ [sys].[tekst_zapytania_magazynu] [qst] WŁ. [qsq].[id_tekstu_zapytania] =[qst].[id_tekstu_zapytania] DOŁĄCZ [sys].[plan_zapytań] [qsp] WŁ. [qsq].[id_zapytania] =[ qsp].[query_id]JOIN [sys].[query_store_runtime_stats] [rs] ON [qsp].[plan_id] =[rs].[plan_id]GDZIE [qsq].[object_id] =OBJECT_ID(N'usp_GetCustomerInfo'); 

Query Przechowywanie danych z zapytania procedury składowanej Zapytanie Przechowywanie danych po wykonaniu procedury składowanej (query_id =1) z dwoma różnymi planami (plan_id =1, plan_id =2)

Plan zapytań dla planu_id =1 (wartość wejściowa =„nazwa”) Plan zapytań dla planu_id =2 (wartość wejściowa ='query_cost')

Gdy mamy już potrzebne informacje w Query Store, możemy sklonować bazę danych (dane Query Store zostaną domyślnie dołączone do klonu):

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

Jak wspomniałem w poprzednim poście CLONEDATABASE, sklonowana baza danych jest przeznaczona do obsługi produktów w celu testowania problemów z wydajnością zapytań. W związku z tym po sklonowaniu jest tylko do odczytu. Wykroczymy poza to, do czego obecnie zaprojektowano DBCC CLONEDATABASE, więc ponownie, chcę tylko przypomnieć o tej uwadze z dokumentacji Microsoft:

Nowo wygenerowana baza danych wygenerowana z DBCC CLONEDATABASE nie jest obsługiwana do użycia jako produkcyjna baza danych i jest przeznaczona głównie do rozwiązywania problemów i celów diagnostycznych.

Aby wprowadzić jakiekolwiek zmiany do testów, muszę wyprowadzić bazę danych z trybu tylko do odczytu. I nie przeszkadza mi to, bo nie planuję używać tego do celów produkcyjnych. Jeśli ta sklonowana baza danych znajduje się w środowisku produkcyjnym, zalecam utworzenie kopii zapasowej i przywrócenie jej na serwerze deweloperskim lub testowym i przeprowadzenie tam testów. Nie polecam testowania w środowisku produkcyjnym ani testowania przeciw instancja produkcyjna (nawet z inną bazą danych).

/* Spraw, aby odczytano zapis (wykonaj kopię zapasową i przywróć w innym miejscu, aby nie pracować w środowisku produkcyjnym)*/ALTER DATABASE [CustomerDB_CLONE] SET READ_WRITE WITH NO_WAIT;

Teraz, gdy jestem w stanie odczytu i zapisu, mogę wprowadzać zmiany, przeprowadzać testy i rejestrować metryki. Zacznę od sprawdzenia, czy mam ten sam plan, co wcześniej (przypominamy, że nie zobaczysz tutaj żadnych wyników, ponieważ w sklonowanej bazie danych nie ma danych):

/* sprawdź, czy otrzymaliśmy ten sam plan */USE [CustomerDB_CLONE];GOEXEC [dbo].[usp_GetCustomerInfo] 'name';GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' WITH RECOMPILE;

Sprawdzając Query Store, zobaczysz tę samą wartość plan_id co wcześniej. Istnieje wiele wierszy dla kombinacji identyfikator_zapytania/id_planu ze względu na różne przedziały czasu, w których zbierano dane (określone przez ustawienie INTERVAL_LENGTH_MINUTES, które ustawiliśmy na 5).

SELECT [qsq].[query_id], [qsp].[plan_id], [qsq].[object_id], [rs].[count_executions], DATEADD(MINUTE, -(DATEDIFF(MINUTE, GETDATE(), GETUTCDATE())), [qsp].[czas_ostatniego_wykonania]) 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]GDZIE [qsq].[object_id] =OBJECT_ID(N'usp_GetCustomerInfo);Idź

Zapytanie Przechowuj dane po wykonaniu procedury składowanej w sklonowanej bazie danych

Testowanie zmian w kodzie

W naszym pierwszym teście przyjrzyjmy się, jak możemy przetestować zmianę w naszym kodzie – w szczególności zmodyfikujemy naszą procedurę składowaną, aby usunąć kolumnę [Aktywny] z listy SELECT.

/* Zmień procedurę za pomocą CREATE OR ALTER (usuń [Active] z zapytania)*/CREATE OR ALTER PROCEDURE [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64))AS SELECT [CustomerID], [FirstName ], [LastName], [Email] FROM [dbo].[Klienci] WHERE [LastName] =@LastName;

Uruchom ponownie procedurę składowaną:

EXEC [dbo].[usp_GetCustomerInfo] 'nazwa' Z RECOMPILE;GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' Z RECOMPILE;

Jeśli zdarzyło Ci się wyświetlić rzeczywisty plan wykonania, zauważysz, że oba zapytania używają teraz tego samego planu, ponieważ zapytanie jest objęte pierwotnie utworzonym indeksem nieklastrowym.

Plan wykonania po zmianie procedury składowanej w celu usunięcia [Aktywny]

Możemy zweryfikować za pomocą Query Store, nasz nowy plan ma identyfikator planu 41:

SELECT [qsq].[query_id], [qsp].[plan_id], [qsq].[object_id], [rs].[count_executions], DATEADD(MINUTE, -(DATEDIFF(MINUTE, GETDATE(), GETUTCDATE())), [qsp].[czas_ostatniego_wykonania]) 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]GDZIE [qsq].[object_id] =OBJECT_ID(N'usp_GetCustomerInfo);

Zapytanie Przechowuj dane po zmianie procedury składowanej

Zauważysz również, że pojawił się nowy identyfikator zapytania (40). Magazyn zapytań wykonuje dopasowanie tekstowe, a my zmieniliśmy tekst zapytania, w ten sposób generowany jest nowy identyfikator zapytania. Należy również zauważyć, że identyfikator_obiektu pozostał ten sam, ponieważ w użyciu użyto składni CREATE OR ALTER. Zróbmy kolejną zmianę, ale użyj DROP, a następnie CREATE OR ALTER.

/* Zmień procedurę za pomocą DROP, a następnie CREATE OR ALTER (połącz [Imię] i [Nazwisko])*/DROP PROCEDURE JEŚLI ISTNIEJE [dbo].[usp_GetCustomerInfo];GO CREATE OR ALTER PROCEDURE [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64))AS SELECT [IDKlienta], RTRIM([Imię]) + ' ' + RTRIM([Nazwisko]), [E-mail] FROM [dbo].[Klienci] WHERE [Nazwisko] =@ Nazwisko;

Teraz ponownie uruchamiamy procedurę:

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

Teraz dane wyjściowe z Query Store stają się bardziej interesujące i zauważ, że mój predykat Query Store zmienił się na 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].[czas_ostatniego_wykonania]) 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]GDZIE [qsq].[object_id] <> 0;

Zapytanie Zapisz dane po zmianie procedury składowanej za pomocą DROP, a następnie CREATE OR ALTER

Obiekt_id został zmieniony na 661577395 i mam nowy query_id (42), ponieważ zmienił się tekst zapytania i nowy plan_id (43). Chociaż ten plan jest nadal wyszukiwaniem indeksu mojego indeksu nieklastrowanego, nadal jest to inny plan w magazynie zapytań. Zrozum, że zalecaną metodą zmiany obiektów podczas korzystania z Query Store jest użycie wzorca ALTER zamiast wzorca DROP i CREATE. Odnosi się to w produkcji i do testowania takich jak ten, ponieważ chcesz zachować ten sam identyfikator_obiektu, aby ułatwić znajdowanie zmian.

Testowanie zmian indeksu

W drugiej części naszych testów, zamiast zmieniać zapytanie, chcemy sprawdzić, czy możemy poprawić wydajność, zmieniając indeks. Zmienimy więc procedurę składowaną z powrotem na oryginalne zapytanie, a następnie zmodyfikujemy indeks.

 CREATE OR ALTER PROCEDURE [dbo]. [usp_GetCustomerInfo] (@LastName [nvarchar](64))AS SELECT [CustomerID], [FirstName], [LastName], [Email], CASE WHEN [Active] =1 THEN „Aktywny” ELSE „Nieaktywny” END [Status] FROM [dbo].[Klienci] WHERE [LastName] =@LastName;GO /* Zmodyfikuj istniejący indeks, aby dodać [Aktywny], aby objąć zapytanie*/UTWÓRZ INDEKS NIESKLASTRAROWANY [Książka telefoniczna_Klienci] ON [dbo].[Klienci]([Nazwisko],[Imię])INCLUDE ([Email], [Active])WITH (DROP_EXISTING=ON);

Ponieważ upuściłem oryginalną procedurę składowaną, pierwotny plan nie znajduje się już w pamięci podręcznej. Gdybym jako pierwszy dokonał zmiany tego indeksu, w ramach testowania, pamiętaj, że zapytanie nie użyje automatycznie nowego indeksu, chyba że wymusiłem ponowną kompilację. Mogę użyć sp_recompile na obiekcie lub mogę nadal używać opcji WITH RECOMPILE w procedurze, aby zobaczyć, że mam ten sam plan z dwiema różnymi wartościami (pamiętaj, że początkowo miałem dwa różne plany). Nie potrzebuję WITH RECOMPILE, ponieważ plan nie znajduje się w pamięci podręcznej, ale zostawiam go włączony ze względu na spójność.

EXEC [dbo].[usp_GetCustomerInfo] 'nazwa' Z RECOMPILE;GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' Z RECOMPILE;

W Query Store widzę inny nowy query_id (ponieważ object_id jest inny niż pierwotnie!) i nowy plan_id:

Zapytanie Przechowuj dane po dodaniu nowego indeksu

Jeśli sprawdzę plan, widzę, że zmodyfikowany indeks jest używany.

Plan zapytań po dodaniu [Active] do indeksu (plan_id =50)

A teraz, gdy mam inny plan, mogę pójść o krok dalej i spróbować zasymulować obciążenie produkcyjne, aby sprawdzić, czy przy różnych parametrach wejściowych ta procedura składowana generuje ten sam plan i używa nowego indeksu. Jest tu jednak zastrzeżenie. Być może zauważyłeś ostrzeżenie dotyczące operatora Index Seek — dzieje się tak, ponieważ nie ma statystyk w kolumnie [LastName]. Kiedy utworzyliśmy indeks z [Active] jako dołączoną kolumną, tabela została odczytana w celu zaktualizowania statystyk. W tabeli brak danych, stąd brak statystyk. Jest to zdecydowanie coś, o czym należy pamiętać podczas testowania indeksu. Gdy brakuje statystyk, optymalizator użyje heurystyki, która może, ale nie musi, przekonać optymalizatora do korzystania z oczekiwanego planu.

Podsumowanie

Jestem wielkim fanem DBCC CLONEDATABASE. Jestem jeszcze większym fanem Query Store. Kiedy połączysz je razem, masz duże możliwości szybkiego testowania zmian w indeksie i kodzie. Dzięki tej metodzie patrzysz przede wszystkim na plany wykonania, aby sprawdzić poprawność ulepszeń. Ponieważ w sklonowanej bazie danych nie ma żadnych danych, nie można rejestrować statystyk wykorzystania zasobów i czasu wykonywania, aby udowodnić lub obalić postrzeganą korzyść w planie wykonania. Nadal musisz przywrócić bazę danych i przetestować na pełnym zestawie danych – a Query Store może nadal być ogromną pomocą w przechwytywaniu danych ilościowych. Jednak w tych przypadkach, w których walidacja planu jest wystarczająca lub dla tych z Was, którzy obecnie nie przeprowadzają żadnych testów, DBCC CLONEDATABASE zapewnia ten łatwy przycisk, którego szukałeś. Query Store sprawia, że ​​proces jest jeszcze łatwiejszy.

Kilka informacji:

Nie polecam używania WITH RECOMPILE podczas wywoływania procedur składowanych (lub deklarowania ich w ten sposób – patrz post Paula White'a). Użyłem tej opcji w tym demo, ponieważ stworzyłem procedurę składowaną wrażliwą na parametry i chciałem się upewnić, że różne wartości generują różne plany i nie używają planu z pamięci podręcznej.

Uruchamianie tych testów w SQL Server 2014 SP2 z DBCC CLONEDATABASE jest całkiem możliwe, ale oczywiście istnieje inne podejście do przechwytywania zapytań i metryk, a także do patrzenia na wydajność. Jeśli chcesz zobaczyć tę samą metodologię testowania bez Query Store, zostaw komentarz i daj mi znać!


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Apache NiFi

  2. Łatwa obsługa CRUD dzięki połączeniu z bazą danych PDO

  3. Prisma, jak odwrócić kolejność

  4. Szacowana liczba wierszy do odczytania

  5. Jak DevOps powinien używać DBaaS (bazy danych jako usługi) do optymalizacji tworzenia aplikacji