Database
 sql >> Base de Dados >  >> RDS >> Database

Usando DBCC CLONEDATABASE e Query Store para teste


No verão passado, após o lançamento do SP2 para SQL Server 2014, escrevi sobre o uso de DBCC CLONEDATABASE para mais do que simplesmente investigar um problema de desempenho de consulta. Um comentário recente no post de um leitor me fez pensar que eu deveria expandir o que eu tinha em mente sobre como usar o banco de dados clonado para testes. Pedro escreveu:
“Sou principalmente um desenvolvedor de C# e enquanto escrevo e lido com T-SQL o tempo todo quando se trata de ir além do SQL Server (praticamente todas as coisas de DBA, estatísticas e afins), eu realmente não sei muito . Nem sei como eu usaria um banco de dados clone como este para ajuste de desempenho”
Bem, Pedro, aqui está. Eu espero que isso ajude!

Configuração


O DBCC CLONEDATABASE foi disponibilizado no SQL Server 2016 SP1, então é isso que usaremos para teste, pois é a versão atual e porque posso usar o Query Store para capturar meus dados. Para facilitar a vida, estou criando um banco de dados para teste, em vez de restaurar uma amostra da Microsoft.
USE [master];GO DROP DATABASE IF EXISTS [CustomerDB], [CustomerDB_CLONE];GO /* Altere os locais dos arquivos conforme apropriado */ CREATE DATABASE [CustomerDB] ON PRIMARY ( NAME =N'CustomerDB', FILENAME =N' C:\Databases\CustomerDB.mdf' , SIZE =512 MB , MAXSIZE =UNLIMITED, FILEGROWTH =65536KB ) LOG ON ( NAME =N'CustomerDB_log', FILENAME =N'C:\Databases\CustomerDB_log.ldf' , SIZE =512 MB , MAXSIZE =UNLIMITED , FILEGROWTH =65536KB );GO ALTER DATABASE [CustomerDB] SET RECOVERY SIMPLE;

Agora, crie uma tabela e adicione alguns dados:
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 /* Isso adiciona 1.000.000 linhas à tabela; sinta-se à vontade para adicionar menos*/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) + '.' + 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 BY 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])INCLUIR ([EMAil]);

Agora, vamos habilitar o Query Store:
USE [master];GO ALTER DATABASE [CustomerDB] SET QUERY_STORE =ON; Altere 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, confly_lest_minutes =5, max_storage_size_mb =256, confly_lery_lest_minutes =5, max_storage_size_mb =256, quedas_lest_lest_minutes =5, max_storage_size_mb =256, que 
Depois de criar e preencher o banco de dados e configurar o Query Store, criaremos um procedimento armazenado para teste:
USE [CustomerDB];GO DROP PROCEDURE SE EXISTE [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].[Customers] WHERE [LastName] =@LastName;

Tome nota:eu usei a nova sintaxe CREATE OR ALTER PROCEDURE que está disponível no SP1.

Executaremos nosso procedimento armazenado algumas vezes para obter alguns dados no Query Store. Adicionei WITH RECOMPILE porque sei que esses dois valores de entrada gerarão planos diferentes e quero ter certeza de capturar os dois.
EXEC [dbo].[usp_GetCustomerInfo] 'name' COM RECOMPILE;GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' COM RECOMPILE;

Se olharmos no Repositório de Consultas, veremos uma consulta de nosso procedimento armazenado e dois planos diferentes (cada um com seu próprio plan_id). Se este fosse um ambiente de produção, teríamos muito mais dados em termos de estatísticas de tempo de execução (duração, IO, informações de CPU) e mais execuções. Mesmo que nossa demonstração tenha menos dados, a teoria é a mesma.
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])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]WHERE [qsq].[object_id] =OBJECT_ID(N'usp_GetCustomerInfo'); 

Consulta dados de armazenamento de consulta de procedimento armazenado Dados de armazenamento de consulta após execução de procedimento armazenado (query_id =1) com dois planos diferentes (plan_id =1, plan_id =2)
Plano de consulta para plan_id =1 (valor de entrada ='nome') Plano de consulta para plan_id =2 (valor de entrada ='query_cost')
Assim que tivermos as informações necessárias no Query Store, podemos clonar o banco de dados (os dados do Query Store serão incluídos no clone por padrão):
DBCC CLONEDATABASE (N'CustomerDB', N'CustomerDB_CLONE');

Como mencionei em minha postagem anterior do CLONEDATABASE, o banco de dados clonado foi projetado para ser usado para suporte ao produto para testar problemas de desempenho de consulta. Como tal, é somente leitura depois de clonado. Vamos além do que o DBCC CLONEDATABASE foi projetado para fazer, então, novamente, quero lembrá-lo desta nota da documentação da Microsoft:
O banco de dados recém-gerado gerado a partir de DBCC CLONEDATABASE não tem suporte para ser usado como um banco de dados de produção e destina-se principalmente para fins de diagnóstico e solução de problemas.
Para fazer qualquer alteração para teste, preciso tirar o banco de dados do modo somente leitura. E estou bem com isso porque não pretendo usar isso para fins de produção. Se esse banco de dados clonado estiver em um ambiente de produção, recomendo que você faça backup e restaure em um dev ou servidor de teste e faça seus testes lá. Eu não recomendo testar em produção, nem recomendo testar contra a instância de produção (mesmo com um banco de dados diferente).
/* Faça-o ler e escrever (faça backup e restaure-o em outro lugar para que você não esteja trabalhando em produção)*/ALTER DATABASE [CustomerDB_CLONE] SET READ_WRITE WITH NO_WAIT;

Agora que estou em um estado de leitura e gravação, posso fazer alterações, fazer alguns testes e capturar métricas. Vou começar verificando se recebo o mesmo plano que fiz antes (lembrete, você não verá nenhuma saída aqui porque não há dados no banco de dados clonado):
/* verifica se temos o mesmo plano */USE [CustomerDB_CLONE];GOEXEC [dbo].[usp_GetCustomerInfo] 'name';GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' WITH RECOMPILE;

Ao verificar o Repositório de Consultas, você verá o mesmo valor plan_id de antes. Existem várias linhas para a combinação query_id/plan_id devido aos diferentes intervalos de tempo em que os dados foram capturados (determinados pela configuração INTERVAL_LENGTH_MINUTES, que definimos como 5).
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], [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]WHERE [qsq].[object_id] =OBJECT_ID(N'usp_GetCustomerInfo');Ir

Consulta os dados do armazenamento após a execução do procedimento armazenado no banco de dados clonado

Mudanças de código de teste


Para nosso primeiro teste, vamos ver como podemos testar uma alteração em nosso código – especificamente, modificaremos nosso procedimento armazenado para remover a coluna [Active] da lista SELECT.
/* Alterar procedimento usando CREATE OR ALTER (remover [Active] da consulta)*/CREATE OR ALTER PROCEDURE [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64))AS SELECT [CustomerID], [FirstName ], [Sobrenome], [E-mail] DE [dbo].[Clientes] ONDE [Sobrenome] =@Sobrenome;

Execute novamente o procedimento armazenado:
EXEC [dbo].[usp_GetCustomerInfo] 'name' COM RECOMPILE;GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' COM RECOMPILE;

Se você exibir o plano de execução real, notará que ambas as consultas agora usam o mesmo plano, pois a consulta é coberta pelo índice não clusterizado que criamos originalmente.

Plano de execução após alterar o procedimento armazenado para remover [Ativo]

Podemos verificar com o Query Store, nosso novo plano tem um plan_id de 41:
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], [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]WHERE [qsq].[object_id] =OBJECT_ID(N'usp_GetCustomerInfo');

Consulta os dados do armazenamento após alterar o procedimento armazenado

Você também notará aqui que há um novo query_id (40). O Query Store realiza a correspondência textual e alteramos o texto da consulta, portanto, um novo query_id é gerado. Observe também que o object_id permaneceu o mesmo, porque use usou a sintaxe CREATE OR ALTER. Vamos fazer outra alteração, mas use DROP e depois CREATE OR ALTER.
/* Altere o procedimento usando DROP e depois CREATE OR ALTER (concatenar [FirstName] e [LastName])*/DROP PROCEDURE IF EXISTS [dbo].[usp_GetCustomerInfo];GO CREATE OR ALTER PROCEDURE [dbo].[usp_GetCustomerInfo] (@LastName [nvarchar](64))AS SELECT [CustomerID], RTRIM([FirstName]) + ' ' + RTRIM([LastName]), [Email] FROM [dbo].[Customers] WHERE [LastName] =@ Sobrenome;

Agora, re-executamos o procedimento:
EXEC [dbo].[usp_GetCustomerInfo] 'name';GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' COM RECOMPILE;

Agora a saída do Query Store fica mais interessante e observe que meu predicado do Query Store mudou para 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].[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])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]WHERE [qsq].[object_id] <> 0;

Consulta os dados do armazenamento após alterar o procedimento armazenado usando DROP e depois CREATE OR ALTER

O object_id mudou para 661577395 e tenho um novo query_id (42) porque o texto da consulta mudou e um novo plan_id (43). Embora esse plano ainda seja uma busca de índice do meu índice não clusterizado, ainda é um plano diferente no Repositório de Consultas. Entenda que o método recomendado para alterar objetos ao usar o Repositório de Consultas é usar ALTER em vez de um padrão DROP e CREATE. Isso é verdade na produção e para testes como esse, pois você deseja manter o object_id o mesmo para facilitar a localização de alterações.

Testar alterações de índice


Para a Parte II de nossos testes, em vez de alterar a consulta, queremos ver se podemos melhorar o desempenho alterando o índice. Portanto, alteraremos o procedimento armazenado de volta para a consulta original e modificaremos o índice.
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].[Customers] WHERE [LastName] =@LastName;GO /* Modifique o índice existente para adicionar [Active] para cobrir a consulta*/CREATE NONCLUSTERED INDEX [PhoneBook_Customers] ATIVADO [dbo].[Clientes]([Sobrenome],[Nome])INCLUIR ([E-mail], [Ativo])COM (DROP_EXISTING=ON);

Como eliminei o procedimento armazenado original, o plano original não está mais em cache. Se eu tivesse feito essa alteração de índice primeiro, como parte do teste, lembre-se de que a consulta não usaria automaticamente o novo índice, a menos que eu forçasse uma recompilação. Eu poderia usar sp_recompile no objeto, ou poderia continuar a usar a opção WITH RECOMPILE no procedimento para ver se obtive o mesmo plano com os dois valores diferentes (lembre-se que eu tinha dois planos diferentes inicialmente). Eu não preciso de WITH RECOMPILE, pois o plano não está em cache, mas estou deixando-o por questão de consistência.
EXEC [dbo].[usp_GetCustomerInfo] 'name' COM RECOMPILE;GOEXEC [dbo].[usp_GetCustomerInfo] 'query_cost' COM RECOMPILE;

Dentro do Query Store, vejo outro novo query_id (porque o object_id é diferente do que era originalmente!) e um novo plan_id:

Dados de armazenamento de consulta após adicionar novo índice

Se eu verificar o plano, posso ver que o índice modificado está sendo usado.

Plano de consulta após [Ativo] adicionado ao índice (plan_id =50)

E agora que tenho um plano diferente, posso dar um passo adiante e tentar simular uma carga de trabalho de produção para verificar se, com parâmetros de entrada diferentes, esse procedimento armazenado gera o mesmo plano e usa o novo índice. Há uma ressalva aqui, no entanto. Você deve ter notado o aviso no operador Index Seek – isso ocorre porque não há estatísticas na coluna [LastName]. Quando criamos o índice com [Ativo] como coluna incluída, a tabela foi lida para atualizar as estatísticas. Não há dados na tabela, daí a falta de estatísticas. Isso é definitivamente algo para se ter em mente com o teste de índice. Quando faltam estatísticas, o otimizador usará heurísticas que podem ou não convencer o otimizador a usar o plano que você espera.

Resumo


Eu sou um grande fã do DBCC CLONEDATABASE. Sou um fã ainda maior do Query Store. Quando você coloca os dois juntos, você tem grande capacidade para testes rápidos de alterações de índice e código. Com esse método, você está analisando principalmente os planos de execução para validar as melhorias. Como não há dados em um banco de dados clonado, você não pode capturar o uso de recursos e estatísticas de tempo de execução para provar ou refutar um benefício percebido em um plano de execução. Você ainda precisa restaurar o banco de dados e testar um conjunto completo de dados – e o Query Store ainda pode ser uma grande ajuda na captura de dados quantitativos. No entanto, para aqueles casos em que a validação do plano é suficiente, ou para aqueles que não fazem nenhum teste atualmente, o DBCC CLONEDATABASE fornece esse botão fácil que você está procurando. O Query Store torna o processo ainda mais fácil.

Alguns itens importantes:

Eu não recomendo usar WITH RECOMPILE ao chamar procedimentos armazenados (ou declará-los dessa maneira – veja o post de Paul White). Usei essa opção para esta demonstração porque criei um procedimento armazenado sensível a parâmetros e queria garantir que os diferentes valores gerassem planos diferentes e não usassem um plano do cache.

A execução desses testes no SQL Server 2014 SP2 com DBCC CLONEDATABASE é bem possível, mas obviamente há uma abordagem diferente para capturar consultas e métricas, além de observar o desempenho. Se você gostaria de ver essa mesma metodologia de teste, sem Query Store, deixe um comentário e me avise!