Minulé léto, po vydání aktualizace SP2 pro SQL Server 2014, jsem psal o použití DBCC CLONEDATABASE k více než pouhému zkoumání problému s výkonem dotazu. Nedávný komentář čtenáře k příspěvku mě přivedl k myšlence, že bych měl rozšířit to, co jsem měl na mysli, jak používat klonovanou databázi k testování. Petr napsal:
„Jsem hlavně vývojář v jazyce C#, a zatímco neustále píšu a zabývám se T-SQL, pokud jde o překračování toho SQL Serveru (v podstatě všechny věci DBA, statistiky a podobně), ve skutečnosti toho moc nevím. . Vlastně ani nevím, jak bych použil klon DB, jako je tento, pro ladění výkonu“Tak Petere, tady to máš. Doufám, že to pomůže!
Nastavení
DBCC CLONEDATABASE byla zpřístupněna v SQL Server 2016 SP1, takže to budeme používat pro testování, protože jde o aktuální vydání, a protože mohu použít Query Store k zachycení svých dat. Abych si usnadnil život, vytvářím databázi pro testování, místo abych obnovoval vzorek od společnosti Microsoft.
POUŽÍVEJTE [master];PŘEJÍT DROP DATABÁZI, POKUD EXISTUJE [CustomerDB], [CustomerDB_CLONE];GO /* Změňte umístění souborů podle potřeby */ VYTVOŘTE DATABÁZI [CustomerDB] NA PRIMÁRNÍM ( NAME =N'CustomerDB', FILENAME =N' C:\Databases\CustomerDB.mdf' , VELIKOST =512 MB, MAXIMÁLNÍ VELIKOST =NEOMEZENÁ, VELIKOST SOUBORU =65536 KB ) PŘIHLÁSIT SE ( JMÉNO =N'CustomerDB_log', NÁZEV SOUBORU =N'C:\DatabasesB'\log1.2MBf =N'C:\DatabasesB'\log1.2MBf MAXSIZE =NEOMEZENO, FILEGROWTH =65536 KB );PŘEJÍT ZMĚNIT DATABÁZI [CustomerDB] NASTAVIT JEDNODUCHÉ OBNOVENÍ;
Nyní vytvořte tabulku a přidejte některá data:
POUŽÍVEJTE [CustomerDB];PŘEJĎTE VYTVOŘIT TABULKU [dbo].[Zákazníci]( [ID zákazníka] [int] NENÍ NULL, [Jméno] [nvarchar](64) NOT NULL, [Příjmení] [nvarchar](64) NOT NULL, [E-mail] [nvarchar](320) NOT NULL, [Aktivní] [bit] NOT NULL DEFAULT 1, [Vytvořeno] [datetime] NOT NULL DEFAULT SYSDATETIME(), [Aktualizováno] [datetime] NULL, CONSTRAINT [PK_Customers] PRIMARY KEY CLUSTERED ([CustomerID]));GO /* To přidá 1 000 000 řádků do tabulky; klidně přidejte méně*/VLOŽTE dbo.Zákazníci S (TABLOCKX) (ID zákazníka, Jméno, Příjmení, E-mail, [Aktivní]) SELECT rn =ŘÁDEK_ČÍSLO() NAD (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 ) OD ( 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(c.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;VYTVOŘIT NEZAHRNUTÝ INDEX [PhoneBook_Customers] NA [dbo].[Customers]([Příjmení] ,[Jméno])INCLUDE ([E-mail]);
Nyní povolíme Query Store:
USE [master];GO ALTER DATABASE [CustomerDB] SET QUERY_STORE =ON; Alter Database [Customerdb] set query_store (operation_mode =read_write, cleup_policy =(stale_query_threshold_days =30), data_flush_interval_seconds =60, interval_length_minutes =5, aworys,> aut,> aut,> aut,> aut,> aworys, awors.Jakmile máme databázi vytvořenou a naplněnou a nakonfigurujeme Query Store, vytvoříme uloženou proceduru pro testování:
POUŽÍVEJTE [CustomerDB];PŘEJÍT POSTUP DROP, POKUD EXISTUJE [dbo].[usp_GetCustomerInfo];VYTVOŘIT NEBO ZMĚNIT POSTUP [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64))JAKO VYBERTE ID zákazníka] [Cust Jméno], [Příjmení], [E-mail], PŘÍPAD KDYŽ [Aktivní] =1 THEN 'Aktivní' ELSE 'Neaktivní' KONEC [Stav] OD [dbo].[Zákazníci] WHERE [Příjmení] =@Příjmení;Vezměte na vědomí:Použil jsem skvělou novou syntaxi CREATE OR ALTER PROCEDURE, která je dostupná v SP1.
Spustíme naši uloženou proceduru několikrát, abychom získali nějaká data v Query Store. Přidal jsem WITH RECOMPILE, protože vím, že tyto dvě vstupní hodnoty vygenerují různé plány, a chci se ujistit, že je zachytím oba.
EXEC [dbo].[usp_GetCustomerInfo] 'name' S PŘEKOMPILOVÁNÍM; GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' S PŘEKOMPILOVÁNÍM;Pokud se podíváme do Query Store, vidíme jeden dotaz z naší uložené procedury a dva různé plány (každý s vlastním plan_id). Pokud by se jednalo o produkční prostředí, měli bychom podstatně více dat, pokud jde o statistiky běhu (trvání, IO, informace o CPU) a více spouštění. I když naše demo má méně dat, teorie je stejná.
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])Z [sys].[query_store_query] [ qsq] PŘIPOJTE SE [sys].[query_store_query_text] [qst] ON [qsq].[query_text_id] =[qst].[query_text_id]PŘIPOJTE SE [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');Dotaz ukládat data z uložené procedury dotaz Dotaz Ukládat data po provedení uložené procedury (query_id =1) se dvěma různými plány (plan_id =1, plan_id =2)
Plán dotazu pro plan_id =1 (vstupní hodnota ='název') Plán dotazu pro plan_id =2 (vstupní hodnota ='query_cost')Jakmile budeme mít informace, které potřebujeme v Query Store, můžeme naklonovat databázi (data Query Store budou ve výchozím nastavení zahrnuta do klonu):
DBCC CLONEDATABASE (N'CustomerDB', N'CustomerDB_CLONE');Jak jsem zmínil ve svém předchozím příspěvku CLONEDATABASE, klonovaná databáze je navržena tak, aby se používala pro podporu produktu k testování problémů s výkonem dotazů. Jako takový je po naklonování pouze pro čtení. Půjdeme nad rámec toho, k čemu je DBCC CLONEDATABASE aktuálně navržena, takže znovu vám chci jen připomenout tuto poznámku z dokumentace společnosti Microsoft:
Nově vygenerovaná databáze generovaná z DBCC CLONEDATABASE není podporována pro použití jako produkční databáze a je primárně určena pro účely odstraňování problémů a diagnostické účely.Aby bylo možné provést jakékoli změny pro testování, musím databázi převést z režimu pouze pro čtení. A jsem s tím v pořádku, protože to neplánuji použít pro výrobní účely. Pokud je tato klonovaná databáze v produkčním prostředí, doporučuji vám ji zálohovat a obnovit na vývojovém nebo testovacím serveru a tam provést testování. Nedoporučuji testovat ve výrobě ani nedoporučuji testovat proti produkční instance (i s jinou databází).
/* Udělejte to pro čtení a zápis (zálohujte to a obnovte to někde jinde, abyste nepracovali ve výrobě)*/ALTER DATABASE [CustomerDB_CLONE] NASTAVTE READ_WRITE S NO_WAIT;Nyní, když jsem ve stavu čtení a zápisu, mohu provádět změny, testovat a zaznamenávat metriky. Začnu ověřením, že dostanu stejný plán jako předtím (připomínáme, že zde neuvidíte žádný výstup, protože v klonované databázi nejsou žádná data):
/* ověřte, že máme stejný plán */POUŽÍVEJTE [CustomerDB_CLONE];GOEXEC [dbo].[usp_GetCustomerInfo] 'název';GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' S PŘEKOMPILOVÁNÍM;Při kontrole Query Store uvidíte stejnou hodnotu plan_id jako předtím. Kombinace query_id/plan_id obsahuje několik řádků kvůli různým časovým intervalům, ve kterých byla data zachycena (určeno nastavením INTERVAL_LENGTH_MINUTES, které jsme nastavili na 5).
SELECT [qsq].[query_id], [qsp].[plan_id], [qsq].[object_id], [rs].[count_executions], DATEADD(MINUTE, -(DATEDIFF(MINUTE, GETDATE(), GETUTCDATE())), [qsp].[čas_posledního_exekuce]) AS [LocalLastExecutionTime], [rsi].[id_intervalu_runtime_stats], [rsi].[čas_zahájení], [rsi].[čas_konce], [qst].[text_query_sql] , ConvertedPlan =TRY_CONVERT(XML, [qsp].[query_plan])Z [sys].[query_store_query] [qsq] PŘIPOJTE SE [sys].[query_store_query_text] [qst] ON [qsq].[query_text_id] =[qst]. [query_text_id]PŘIPOJTE SE [sys].[query_store_plan] [qsp] ON [qsq].[query_id] =[qsp].[query_id]PŘIPOJTE SE [sys].[query_store_runtime_stats] [rs] ON [qsp].[plan_id] =[rs].[plan_id]PŘIPOJTE SE [sys].[query_store_runtime_stats_interval] [rsi] ON [rs].[runtime_stats_interval_id] =[rsi].[runtime_stats_interval_id]KDE [qsq].[object_id] =OBJECT_ID(N'usp_Info'Cust);GODotaz ukládat data po provedení uložené procedury proti klonované databázi
Změny testovacího kódu
V našem prvním testu se podíváme na to, jak bychom mohli otestovat změnu našeho kódu – konkrétně upravíme naši uloženou proceduru tak, abychom odstranili sloupec [Active] ze seznamu SELECT.
/* Změňte proceduru pomocí CREATE OR ALTER (odstraňte [Aktivní] z dotazu)*/CREATE OR ALTER PROCEDURE [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64))AS SELECT [CustomerID], [FirstName ], [Příjmení], [E-mail] OD [dbo].[Zákazníci] KDE [Příjmení] =@Příjmení;Znovu spusťte uloženou proceduru:
EXEC [dbo].[usp_GetCustomerInfo] 'name' S PŘEKOMPILOVÁNÍM; GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' S PŘEKOMPILOVÁNÍM;Pokud jste náhodou zobrazili skutečný plán provádění, všimnete si, že oba dotazy nyní používají stejný plán, protože dotaz je pokryt neshlukovaným indexem, který jsme původně vytvořili.
Plán provádění po změně uložené procedury k odstranění [Aktivní]
Můžeme ověřit pomocí Query Store, náš nový plán má plan_id 41:
SELECT [qsq].[query_id], [qsp].[plan_id], [qsq].[object_id], [rs].[count_executions], DATEADD(MINUTE, -(DATEDIFF(MINUTE, GETDATE(), GETUTCDATE())), [qsp].[čas_posledního_exekuce]) AS [LocalLastExecutionTime], [rsi].[id_intervalu_runtime_stats], [rsi].[čas_zahájení], [rsi].[čas_konce], [qst].[text_query_sql] , ConvertedPlan =TRY_CONVERT(XML, [qsp].[query_plan])Z [sys].[query_store_query] [qsq] PŘIPOJTE SE [sys].[query_store_query_text] [qst] ON [qsq].[query_text_id] =[qst]. [query_text_id]PŘIPOJTE SE [sys].[query_store_plan] [qsp] ON [qsq].[query_id] =[qsp].[query_id]PŘIPOJTE SE [sys].[query_store_runtime_stats] [rs] ON [qsp].[plan_id] =[rs].[plan_id]PŘIPOJTE SE [sys].[query_store_runtime_stats_interval] [rsi] ON [rs].[runtime_stats_interval_id] =[rsi].[runtime_stats_interval_id]KDE [qsq].[object_id] =OBJECT_ID(N'usp_Info'Cust);Ukládání dotazů na data po změně uložené procedury
Zde si také všimnete, že je zde nový query_id (40). Query Store provádí textové párování a my jsme změnili text dotazu, takže je vygenerováno nové query_id. Všimněte si také, že object_id zůstalo stejné, protože use použilo syntaxi CREATE OR ALTER. Udělejme další změnu, ale použijte DROP a poté CREATE OR ALTER.
/* Změňte proceduru pomocí DROP a potom CREATE OR ALTER (zřetězit [Jméno] a [Příjmení])*/POSTUP DROP, POKUD EXISTUJE [dbo].[usp_GetCustomerInfo];PŘEJÍT POSTUP VYTVOŘIT NEBO ZMĚNIT [dbo].[usp_GetCustomerInfo]CustomerInfo] (@Příjmení [nvarchar](64))JAK VYBERTE [ID zákazníka], RTRIM([Jméno]) + ' ' + RTRIM([Příjmení]), [E-mail] OD [dbo].[Zákazníci] WHERE [Příjmení] =@ Příjmení;Nyní znovu spustíme postup:
EXEC [dbo].[usp_GetCustomerInfo] 'název';GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' S PŘEKOMPILOVÁNÍM;Nyní je výstup z Query Store zajímavější a všimněte si, že můj predikát Query Store se změnil na WHERE [qsq].[id_objektu] <> 0.
SELECT [qsq].[query_id], [qsp].[plan_id], [qsq].[object_id], [rs].[count_executions], DATEADD(MINUTE, -(DATEDIFF(MINUTE, GETDATE(), GETUTCDATE())), [qsp].[čas_posledního_exekuce]) AS [LocalLastExecutionTime], [rsi].[id_intervalu_runtime_stats], [rsi].[čas_zahájení], [rsi].[čas_konce], [qst].[text_query_sql] , ConvertedPlan =TRY_CONVERT(XML, [qsp].[query_plan])Z [sys].[query_store_query] [qsq] PŘIPOJTE SE [sys].[query_store_query_text] [qst] ON [qsq].[query_text_id] =[qst]. [query_text_id]PŘIPOJTE SE [sys].[query_store_plan] [qsp] ON [qsq].[query_id] =[qsp].[query_id]PŘIPOJTE SE [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]KDE [qsq].[object_id] <> 0;Dotaz na uložení dat po změně uložené procedury pomocí DROP a poté CREATE OR ALTER
Object_id se změnilo na 661577395 a mám nové query_id (42), protože se změnil text dotazu, a nové plan_id (43). I když je tento plán stále hledáním indexu mého neshlukovaného indexu, v Query Store je to stále jiný plán. Pochopte, že doporučenou metodou pro změnu objektů, když používáte Query Store, je použít ALTER spíše než vzor DROP a CREATE. To platí v produkci a pro testování, jako je toto, protože chcete zachovat object_id stejné, abyste usnadnili hledání změn.
Testování změn indexu
U části II našeho testování chceme spíše než změnu dotazu zjistit, zda můžeme zlepšit výkon změnou indexu. Uloženou proceduru tedy změníme zpět na původní dotaz a poté upravíme index.
VYTVOŘTE NEBO ZMĚŇTE POSTUP [dbo].[usp_GetCustomerInfo] (@Příjmení [nvarchar](64))JAK VYBERTE [ID zákazníka], [Jméno], [Příjmení], [E-mail], PŘÍPAD, KDYŽ [Aktivní] =1 POTOM 'Aktivní' ELSE 'Neaktivní' KONEC [Stav] OD [dbo].[Customers] WHERE [LastName] =@LastName;GO /* Upravte stávající index a přidejte [Aktivní], aby pokryl dotaz*/VYTVOŘIT NEZAHRNUTÝ INDEX [PhoneBook_Customers] ON [dbo].[Customers]([LastName],[FirstName])INCLUDE ([E-mail], [Active])WITH (DROP_EXISTING=ON);Protože jsem zrušil původní uloženou proceduru, původní plán již není v mezipaměti. Pokud bych tuto změnu indexu provedl jako první, v rámci testování, pamatujte, že dotaz by automaticky nepoužil nový index, pokud bych nevynutil rekompilaci. Mohl bych použít sp_recompile na objekt nebo bych mohl pokračovat v používání možnosti WITH RECOMPILE v postupu, abych viděl, že mám stejný plán se dvěma různými hodnotami (nezapomeňte, že jsem měl zpočátku dva různé plány). Nepotřebuji WITH RECOMPILE, protože plán není v mezipaměti, ale kvůli konzistenci ho nechávám zapnutý.
EXEC [dbo].[usp_GetCustomerInfo] 'name' S PŘEKOMPILOVÁNÍM; GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' S PŘEKOMPILOVÁNÍM;V obchodě Query Store vidím další nové query_id (protože object_id je jiné, než bylo původně!) a nové plan_id:
Ukládání dotazů na data po přidání nového indexu
Když zkontroluji plán, vidím, že se používá upravený index.
Plán dotazů po přidání [Aktivní] do indexu (plan_id =50)
A teď, když mám jiný plán, mohl bych to udělat o krok dále a zkusit simulovat produkční zátěž, abych ověřil, že s různými vstupními parametry tato uložená procedura generuje stejný plán a používá nový index. Je tu však upozornění. Možná jste si všimli varování na operátoru Index Seek – k tomu dochází, protože ve sloupci [LastName] nejsou žádné statistiky. Když jsme vytvořili index s [Aktivní] jako zahrnutým sloupcem, byla tabulka načtena za účelem aktualizace statistik. V tabulce nejsou žádná data, proto chybí statistika. To je určitě něco, co je třeba mít na paměti při testování indexů. Pokud statistiky chybí, optimalizátor použije heuristiku, která může, ale nemusí optimalizátora přesvědčit, aby použil plán, který očekáváte.
Shrnutí
Jsem velkým fanouškem DBCC CLONEDATABASE. Jsem ještě větší fanoušek Query Store. Když je dáte dohromady, máte skvělé možnosti pro rychlé testování změn indexu a kódu. S touto metodou se primárně díváte na prováděcí plány k ověření vylepšení. Protože v klonované databázi nejsou žádná data, nemůžete zaznamenat statistiky využití prostředků a běhu, abyste prokázali nebo vyvrátili vnímanou výhodu v plánu provádění. Stále potřebujete obnovit databázi a testovat s úplnou sadou dat – a Query Store může být stále velkým pomocníkem při získávání kvantitativních dat. Nicméně pro případy, kdy je ověření plánu dostatečné, nebo pro ty z vás, kteří v současné době neprovádějí žádné testování, nabízí DBCC CLONEDATABASE to snadné tlačítko, které jste hledali. Query Store tento proces ještě usnadňuje.
Několik poznámek:
Nedoporučuji používat WITH RECOMPILE při volání uložených procedur (nebo je takto deklarovat – viz příspěvek Paula Whitea). Tuto možnost jsem použil pro toto demo, protože jsem vytvořil uloženou proceduru citlivou na parametry a chtěl jsem se ujistit, že různé hodnoty generují různé plány a nepoužívají plán z mezipaměti.
Spuštění těchto testů v SQL Server 2014 SP2 s DBCC CLONEDATABASE je docela možné, ale samozřejmě existuje jiný přístup k zachycování dotazů a metrik a také ke sledování výkonu. Pokud byste chtěli vidět stejnou metodologii testování bez Query Store, zanechte komentář a dejte mi vědět!