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

Utilizzo di DBCC CLONEDATABASE e Query Store per il test

L'estate scorsa, dopo il rilascio di SP2 per SQL Server 2014, ho scritto sull'utilizzo di DBCC CLONEDATABASE per qualcosa di più della semplice analisi di un problema di prestazioni delle query. Un recente commento al post di un lettore mi ha fatto pensare che avrei dovuto espandere ciò che avevo in mente su come utilizzare il database clonato per i test. Pietro ha scritto:

"Sono principalmente uno sviluppatore C# e mentre scrivo e mi occupo di T-SQL tutto il tempo quando si tratta di andare oltre SQL Server (praticamente tutte le cose DBA, statistiche e simili) non so davvero molto . Non so nemmeno come userei un DB clone come questo per l'ottimizzazione delle prestazioni”

Bene Peter, ecco a te. Spero che questo aiuti!

Configurazione

DBCC CLONEDATABASE è stato reso disponibile in SQL Server 2016 SP1, quindi è ciò che useremo per i test poiché è la versione corrente e perché posso usare Query Store per acquisire i miei dati. Per semplificarti la vita, sto creando un database per il test, invece di ripristinare un campione da Microsoft.

USE [master];GO DROP DATABASE IF EXISTS [CustomerDB], [CustomerDB_CLONE];GO /* Modifica le posizioni dei file come appropriato */ CREATE DATABASE [CustomerDB] ON PRIMARY ( NAME =N'CustomerDB', FILENAME =N' C:\Databases\CustomerDB.mdf' , SIZE =512MB , MAXSIZE =UNLIMITED, FILEGROWTH =65536KB ) ACCEDI ( NAME =N'CustomerDB_log', FILENAME =N'C:\Databases\CustomerDB_log.ldf' , SIZE =512 MB , MAXSIZE =ILLIMITATO , FILEGROWTH =65536KB );VAI ALTER DATABASE [CustomerDB] IMPOSTA RECUPERO SEMPLICE;

Ora crea una tabella e aggiungi alcuni dati:

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 /* Aggiunge 1.000.000 di righe alla tabella; sentiti libero di aggiungere 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 ) DA ( SELEZIONA 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 PER 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] ,[Nome])INCLUDE ([E-Mail]);

Ora abiliteremo Query Store:

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 =TUTTO, SIZE_BASED_2000PERQUE_BASED_, MAX_SPLAN); 

Dopo aver creato e popolato il database e aver configurato Query Store, creeremo una stored procedure per il test:

UTILIZZA [CustomerDB];GO DROP PROCEDURE SE ESISTE [dbo].[usp_GetCustomerInfo];GO CREATE OR ALTER PROCEDURE [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64))AS SELECT [CustomerID], [ Nome], [Cognome], [E-mail], CASO QUANDO [Attivo] =1 ALLORA 'Attivo' ELSE 'Non attivo' FINE [Stato] DA [dbo].[Clienti] DOVE [Cognome] =@Cognome;

Prendi nota:ho usato la nuova fantastica sintassi CREATE OR ALTER PROCEDURE disponibile in SP1.

Eseguiremo la nostra procedura memorizzata un paio di volte per ottenere alcuni dati in Query Store. Ho aggiunto WITH RECOMPILE perché so che questi due valori di input genereranno piani diversi e voglio assicurarmi di catturarli entrambi.

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

Se guardiamo in Query Store, vediamo una query dalla nostra procedura memorizzata e due piani diversi (ciascuno con il proprio plan_id). Se questo fosse un ambiente di produzione, avremmo molti più dati in termini di statistiche di runtime (durata, IO, informazioni sulla CPU) e più esecuzioni. Anche se la nostra demo ha meno dati, la teoria è la stessa.

SELECT [qsq].[query_id], [qsp].[plan_id], [qsq].[object_id], [rs].[count_esecuzioni], DATAADD(MINUTE, -(DATEDIFF(MINUTE, GETDATE(), GETUTCDATE())), [qsp].[last_execution_time]) AS [LocalLastExecutionTime], [qst].[query_sql_text], ConvertedPlan =TRY_CONVERT(XML, [qsp].[query_plan])DA [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 Archivia i dati dalla query della procedura memorizzata Query Archivia i dati dopo l'esecuzione della procedura memorizzata (query_id =1) con due piani diversi (plan_id =1, plan_id =2)

Piano di query per plan_id =1 (valore di input ='nome') Piano di query per plan_id =2 (valore di input ='query_cost')

Una volta che abbiamo le informazioni di cui abbiamo bisogno in Query Store, possiamo clonare il database (i dati di Query Store saranno inclusi nel clone per impostazione predefinita):

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

Come accennato nel mio precedente post CLONEDATABASE, il database clonato è progettato per essere utilizzato per il supporto del prodotto per testare i problemi di prestazioni delle query. In quanto tale, è di sola lettura dopo essere stato clonato. Andremo oltre ciò per cui DBCC CLONEDATABASE è attualmente progettato per fare, quindi, ancora una volta, voglio solo ricordarti questa nota dalla documentazione Microsoft:

Il database appena generato generato da DBCC CLONEDATABASE non è supportato per l'uso come database di produzione ed è destinato principalmente a scopi diagnostici e di risoluzione dei problemi.

Per apportare modifiche ai test, è necessario rimuovere il database dalla modalità di sola lettura. E mi va bene perché non ho intenzione di usarlo per scopi di produzione. Se questo database clonato si trova in un ambiente di produzione, ti consiglio di eseguirne il backup e di ripristinarlo su un server di sviluppo o test e di eseguire i test lì. Non consiglio di testare in produzione, né di testare contro l'istanza di produzione (anche con un database diverso).

/* Fallo leggere e scrivere (esegui il backup e ripristinalo da qualche altra parte in modo da non lavorare in produzione)*/ALTER DATABASE [CustomerDB_CLONE] SET READ_WRITE WITH NO_WAIT;

Ora che sono in uno stato di lettura-scrittura, posso apportare modifiche, eseguire alcuni test e acquisire metriche. Inizierò verificando di ottenere lo stesso piano che avevo prima (ricorda, non vedrai alcun output qui perché non ci sono dati nel database clonato):

/* verifica che otteniamo lo stesso piano */USE [CustomerDB_CLONE];GOEXEC [dbo].[usp_GetCustomerInfo] 'name';GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' WITH RECOMPILE;

Controllando Query Store, vedrai lo stesso valore plan_id di prima. Esistono più righe per la combinazione query_id/plan_id a causa dei diversi intervalli di tempo durante i quali i dati sono stati acquisiti (determinati dall'impostazione INTERVAL_LENGTH_MINUTES, che abbiamo impostato su 5).

SELECT [qsq].[query_id], [qsp].[plan_id], [qsq].[object_id], [rs].[count_esecuzioni], DATAADD(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])DA [sys].[query_store_query] [qsq] UNISCITI a [sys].[query_store_query_text] [qst] ON [qsq].[query_text_id] =[qst]. [query_text_id]ISCRIVITI [sys].[query_store_plan] [qsp] ON [qsq].[query_id] =[qsp].[query_id]ISCRIVITI [sys].[query_store_runtime_stats] [rs] ON [qsp].[plan_id] =[rs].[plan_id]ISCRIVITI [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');Vai

Query sui dati dell'archivio dopo l'esecuzione della procedura memorizzata sul database clonato

Test delle modifiche al codice

Per il nostro primo test, diamo un'occhiata a come testare una modifica al nostro codice, in particolare modificheremo la nostra procedura memorizzata per rimuovere la colonna [Attivo] dall'elenco SELECT.

/* Modifica procedura utilizzando CREATE OR ALTER (rimuovere [Attivo] dalla query)*/CREATE OR ALTER PROCEDURE [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64))AS SELECT [CustomerID], [FirstName ], [Cognome], [E-mail] DA [dbo].[Clienti] DOVE [Cognome] =@Cognome;

Esegui nuovamente la procedura memorizzata:

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

Se ti è capitato di visualizzare il piano di esecuzione effettivo, noterai che entrambe le query ora utilizzano lo stesso piano, poiché la query è coperta dall'indice non cluster creato originariamente.

Piano di esecuzione dopo aver modificato la procedura memorizzata per rimuovere [Attivo]

Possiamo verificare con Query Store, il nostro nuovo piano ha un plan_id di 41:

SELECT [qsq].[query_id], [qsp].[plan_id], [qsq].[object_id], [rs].[count_esecuzioni], DATAADD(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])DA [sys].[query_store_query] [qsq] UNISCITI a [sys].[query_store_query_text] [qst] ON [qsq].[query_text_id] =[qst]. [query_text_id]ISCRIVITI [sys].[query_store_plan] [qsp] ON [qsq].[query_id] =[qsp].[query_id]ISCRIVITI [sys].[query_store_runtime_stats] [rs] ON [qsp].[plan_id] =[rs].[plan_id]ISCRIVITI [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 Store dati dopo aver modificato la procedura memorizzata

Noterai anche qui che c'è un nuovo query_id (40). Query Store esegue la corrispondenza testuale e abbiamo modificato il testo della query, quindi viene generato un nuovo query_id. Si noti inoltre che object_id è rimasto lo stesso, perché use utilizzava la sintassi CREATE OR ALTER. Facciamo un'altra modifica, ma usa DROP e poi CREATE OR ALTER.

/* Modificare la procedura utilizzando DROP e quindi CREATE OR ALTER (concatenare [FirstName] e [LastName])*/DROP PROCEDURE IF EXISTS [dbo].[usp_GetCustomerInfo];GO CREATE OR ALTER PROCEDURE [dbo].[usp_GetCustomerInfo] (@Cognome [nvarchar](64))AS SELECT [IDCliente], RTRIM([Nome]) + ' ' + RTRIM([Cognome]), [E-mail] DA [dbo].[Clienti] DOVE [Cognome] =@ Cognome;

Ora, rieseguiamo la procedura:

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

Ora l'output di Query Store diventa più interessante e nota che il mio predicato Query Store è cambiato in WHERE [qsq].[object_id] <> 0.

SELECT [qsq].[query_id], [qsp].[plan_id], [qsq].[object_id], [rs].[count_esecuzioni], DATAADD(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])DA [sys].[query_store_query] [qsq] UNISCITI a [sys].[query_store_query_text] [qst] ON [qsq].[query_text_id] =[qst]. [query_text_id]ISCRIVITI [sys].[query_store_plan] [qsp] ON [qsq].[query_id] =[qsp].[query_id]ISCRIVITI [sys].[query_store_runtime_stats] [rs] ON [qsp].[plan_id] =[rs].[plan_id]ISCRIVITI [sys].[query_store_runtime_stats_interval] [rsi] ON [rs].[runtime_stats_interval_id] =[rsi].[runtime_stats_interval_id]WHERE [qsq].[object_id] <> 0;

Query Archivia i dati dopo aver modificato la procedura memorizzata utilizzando DROP e quindi CREATE OR ALTER

Object_id è cambiato in 661577395 e ho un nuovo query_id (42) perché il testo della query è cambiato e un nuovo plan_id (43). Sebbene questo piano sia ancora una ricerca dell'indice del mio indice non cluster, è ancora un piano diverso in Query Store. Tieni presente che il metodo consigliato per modificare gli oggetti quando utilizzi Query Store consiste nell'usare ALTER anziché un pattern DROP e CREATE. Questo è vero in produzione e per test come questo, poiché vuoi mantenere lo stesso object_id per facilitare la ricerca delle modifiche.

Test delle modifiche all'indice

Per la parte II del nostro test, invece di modificare la query, vogliamo vedere se possiamo migliorare le prestazioni modificando l'indice. Quindi riporteremo la stored procedure alla query originale, quindi modificheremo l'indice.

CREATE O ALTER PROCEDURE [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64))AS SELECT [CustomerID], [FirstName], [LastName], [Email], CASE WHEN [Active] =1 THEN 'Attivo' ELSE 'Non attivo' FINE [Stato] DA [dbo].[Clienti] WHERE [Cognome] =@Cognome;Vai /* Modifica l'indice esistente per aggiungere [Attivo] per coprire la query*/CREA INDICE NON CLUSTERED [Rubrica_Clienti] ON [dbo].[Clienti]([Cognome],[Nome])INCLUDE ([EMail], [Attivo])CON (DROP_EXISTING=ON);

Poiché ho abbandonato la stored procedure originale, il piano originale non è più nella cache. Se avessi apportato prima questa modifica all'indice, come parte del test, ricorda che la query non utilizzerà automaticamente il nuovo indice a meno che non abbia forzato una ricompilazione. Potrei usare sp_recompile sull'oggetto, oppure potrei continuare a usare l'opzione WITH RECOMPILE sulla procedura per vedere che ho ottenuto lo stesso piano con due valori diversi (ricorda che inizialmente avevo due piani diversi). Non ho bisogno di WITH RECOMPILE perché il piano non è nella cache, ma lo lascio acceso per motivi di coerenza.

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

All'interno di Query Store vedo un altro nuovo query_id (perché l'object_id è diverso da come era originariamente!) e un nuovo plan_id:

Query Store dati dopo l'aggiunta di un nuovo indice

Se controllo il piano, vedo che viene utilizzato l'indice modificato.

Piano di query dopo l'aggiunta di [Attivo] all'indice (plan_id =50)

E ora che ho un piano diverso, potrei fare un ulteriore passo avanti e provare a simulare un carico di lavoro di produzione per verificare che con parametri di input diversi, questa stored procedure generi lo stesso piano e utilizzi il nuovo indice. C'è un avvertimento qui, però. Potresti aver notato l'avviso sull'operatore Index Seek:ciò si verifica perché non ci sono statistiche nella colonna [LastName]. Quando abbiamo creato l'indice con [Attivo] come colonna inclusa, la tabella è stata letta per aggiornare le statistiche. Non ci sono dati nella tabella, da qui la mancanza di statistiche. Questo è sicuramente qualcosa da tenere a mente con il test dell'indice. Quando mancano le statistiche, l'ottimizzatore utilizzerà l'euristica che potrebbe convincere o meno l'ottimizzatore a utilizzare il piano previsto.

Riepilogo

Sono un grande fan di DBCC CLONEDATABASE. Sono un fan ancora più grande di Query Store. Quando li metti insieme, hai una grande capacità per testare rapidamente le modifiche all'indice e al codice. Con questo metodo, stai principalmente esaminando i piani di esecuzione per convalidare i miglioramenti. Poiché non sono presenti dati in un database clonato, non è possibile acquisire l'utilizzo delle risorse e le statistiche di runtime per provare o smentire un vantaggio percepito in un piano di esecuzione. È ancora necessario ripristinare il database e testare un set completo di dati e Query Store può comunque essere di grande aiuto nell'acquisizione di dati quantitativi. Tuttavia, per quei casi in cui la convalida del piano è sufficiente o per quelli di voi che al momento non eseguono alcun test, DBCC CLONEDATABASE fornisce quel semplice pulsante che stavi cercando. Query Store rende il processo ancora più semplice.

Alcuni elementi da notare:

Non consiglio di utilizzare WITH RECOMPILE quando si chiamano le stored procedure (o le dichiarano in questo modo - vedere il post di Paul White). Ho usato questa opzione per questa demo perché ho creato una procedura memorizzata sensibile ai parametri e volevo assicurarmi che i diversi valori generassero piani diversi e non utilizzassero un piano dalla cache.

L'esecuzione di questi test in SQL Server 2014 SP2 con DBCC CLONEDATABASE è del tutto possibile, ma esiste ovviamente un approccio diverso per l'acquisizione di query e metriche, nonché per l'analisi delle prestazioni. Se desideri vedere questa stessa metodologia di test, senza Query Store, lascia un commento e fammi sapere!