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

Um caso de uso para sp_prepare / sp_prepexec


Existem recursos que muitos de nós evitam, como cursores, gatilhos e SQL dinâmico. Não há dúvida de que cada um deles tem seus casos de uso, mas quando vemos um gatilho com um cursor dentro do SQL dinâmico, isso pode nos fazer estremecer (golpe triplo).

Plan guides e sp_prepare estão em um barco semelhante:se você me visse usando um deles, você levantaria uma sobrancelha; se você me visse usando eles juntos, provavelmente verificaria minha temperatura. Mas, assim como cursores, gatilhos e SQL dinâmico, eles têm seus casos de uso. E recentemente me deparei com um cenário em que usá-los juntos era benéfico.

Plano de fundo


Temos muitos dados. E muitos aplicativos rodando contra esses dados. Alguns desses aplicativos são difíceis ou impossíveis de alterar, principalmente aplicativos de prateleira de terceiros. Portanto, quando seu aplicativo compilado envia consultas ad hoc para o SQL Server, principalmente como uma instrução preparada, e quando não temos a liberdade de adicionar ou alterar índices, várias oportunidades de ajuste são imediatamente descartadas.

Nesse caso, tínhamos uma tabela com alguns milhões de linhas. Uma versão simplificada e higienizada:
CREATE TABLE dbo.TheThings
(
  ThingID    bigint NOT NULL,
  TypeID     uniqueidentifier NOT NULL,
  dt1        datetime NOT NULL DEFAULT sysutcdatetime(),
  dt2        datetime NOT NULL DEFAULT sysutcdatetime(),
  dt3        datetime NOT NULL DEFAULT sysutcdatetime(),
  CONSTRAINT PK_TheThings PRIMARY KEY (ThingID)
);
 
CREATE INDEX ix_type ON dbo.TheThings(TypeID);
 
SET NOCOUNT ON;
GO
 
DECLARE @guid1 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4',
        @guid2 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';
 
INSERT dbo.TheThings(ThingID, TypeID)
  SELECT TOP (1000) 1000 + ROW_NUMBER() OVER (ORDER BY name), @guid1
    FROM sys.all_columns;
 
INSERT dbo.TheThings(ThingID, TypeID)
  SELECT TOP (1) 2500, @guid2
    FROM sys.all_columns;
 
INSERT dbo.TheThings(ThingID, TypeID)
  SELECT TOP (1000) 3000 + ROW_NUMBER() OVER (ORDER BY name), @guid1
    FROM sys.all_columns;

A instrução preparada do aplicativo ficou assim (como visto no cache do plano):
(@P0 varchar(8000))SELECT * FROM dbo.TheThings WHERE TypeID = @P0

O problema é que, para alguns valores de TypeID , haveria muitos milhares de linhas. Para outros valores, haveria menos de 10. Se o plano errado for escolhido (e reutilizado) com base em um tipo de parâmetro, isso pode ser um problema para os outros. Para a consulta que recupera um punhado de linhas, queremos uma busca de índice com pesquisas para recuperar as colunas não cobertas adicionais, mas para a consulta que retorna 700 mil linhas, queremos apenas uma verificação de índice clusterizado. (Idealmente, o índice cobriria, mas esta opção não estava nos cartões desta vez.)

Na prática, o aplicativo estava sempre recebendo a variação de varredura, mesmo sendo essa a que era necessária em cerca de 1% das vezes. 99% das consultas estavam usando uma varredura de 2 milhões de linhas quando poderiam ter usado uma busca + 4 ou 5 pesquisas.

Poderíamos reproduzir isso facilmente no Management Studio executando esta consulta:
DBCC FREEPROCCACHE;
DECLARE @P0 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4';
SELECT * FROM dbo.TheThings WHERE TypeID = @P0;
GO
 
DBCC FREEPROCCACHE;
DECLARE @P0 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';
SELECT * FROM dbo.TheThings WHERE TypeID = @P0;
GO

Os planos voltaram assim:

A estimativa em ambos os casos foi de 1.000 linhas; os avisos à direita são devidos a E/S residual.

Como podemos ter certeza de que a consulta fez a escolha certa dependendo do parâmetro? Precisaríamos fazê-lo recompilar, sem adicionar dicas à consulta, ativar sinalizadores de rastreamento ou alterar as configurações do banco de dados.

Se eu executasse as consultas de forma independente usando OPTION (RECOMPILE) , eu obteria a busca quando apropriado:
DBCC FREEPROCCACHE;
 
DECLARE @guid1 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4',
        @guid2 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';
 
SELECT * FROM dbo.TheThings WHERE TypeID = @guid1 OPTION (RECOMPILE);
SELECT * FROM dbo.TheThings WHERE TypeID = @guid2 OPTION (RECOMPILE);

Com o RECOMPILE, obtemos estimativas mais precisas e procuramos quando precisamos.

Mas, novamente, não pudemos adicionar a dica à consulta diretamente.

Vamos tentar um guia de plano


Muitas pessoas advertem contra os guias de planos, mas estávamos meio encurralados aqui. Definitivamente, preferiríamos alterar a consulta ou os índices, se pudéssemos. Mas esta pode ser a próxima melhor coisa.
EXEC sys.sp_create_plan_guide   
  @name   = N'TheThingGuide',
  @stmt   = N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0',
  @type   = N'SQL',
  @params = N'@P0 varchar(8000)',
  @hints  = N'OPTION (RECOMPILE)';

Parece direto; testá-lo é o problema. Como simulamos uma instrução preparada no Management Studio? Como podemos ter certeza de que o aplicativo está recebendo o plano guiado e que é explicitamente por causa do guia do plano?

Se tentarmos simular essa consulta no SSMS, isso será tratado como uma instrução ad hoc, não como uma instrução preparada, e não consegui fazer isso para pegar o guia do plano:
DECLARE @P0 varchar(8000) = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; -- also tried uniqueidentifier
SELECT * FROM dbo.TheThings WHERE TypeID = @P0

O SQL dinâmico também não funcionou (isso também foi tratado como uma instrução ad hoc):
DECLARE @sql nvarchar(max) = N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0', 
        @params nvarchar(max) = N'@P0 varchar(8000)', -- also tried uniqueidentifier
        @P0 varchar(8000) = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';
 
EXEC sys.sp_executesql @sql, @params, @P0;

E eu não poderia fazer isso, porque ele também não pegaria o guia do plano (a parametrização assume aqui, e eu não tinha a liberdade de alterar as configurações do banco de dados, mesmo que isso fosse tratado como uma declaração preparada) :
SELECT * FROM TheThings WHERE TypeID = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';

Não consigo verificar o cache do plano para as consultas em execução no aplicativo, pois o plano em cache não indica nada sobre o uso do guia do plano (o SSMS injeta essas informações no XML para você quando você gera um plano real). E se a consulta estiver realmente observando a dica RECOMPILE que estou passando para o guia do plano, como eu poderia ver alguma evidência no cache do plano?

Vamos tentar sp_prepare


Usei sp_prepare menos em minha carreira do que guias de plano e não recomendaria usá-lo para código de aplicativo. (Como Erik Darling aponta, a estimativa pode ser extraída do vetor de densidade, não de cheirar o parâmetro.)

No meu caso, não quero usá-lo por questões de desempenho, quero usá-lo (junto com sp_execute) para simular a instrução preparada vinda do aplicativo.
DECLARE @o int;
EXEC sys.sp_prepare @o OUTPUT, N'@P0 varchar(8000)',
     N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0';
 
EXEC sys.sp_execute @o,  'EE81197A-B2EA-41F4-882E-4A5979ACACE4'; -- PK scan
EXEC sys.sp_execute @o,  'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; -- IX seek + lookup

O SSMS mostra que o guia do plano foi usado em ambos os casos.

Você não poderá verificar o cache do plano para esses resultados, devido à recompilação. Mas em um cenário como o meu, você deve ser capaz de ver os efeitos no monitoramento, verificando explicitamente por meio de eventos estendidos ou observando o alívio do sintoma que fez você investigar essa consulta em primeiro lugar (apenas esteja ciente de que o tempo médio de execução, consulta estatísticas, etc., podem ser afetadas pela compilação adicional).

Conclusão


Este foi um caso em que um guia de plano foi benéfico e sp_prepare foi útil para validar se funcionaria para o aplicativo. Estes não são frequentemente úteis, e menos frequentemente juntos, mas para mim foi uma combinação interessante. Mesmo sem o guia de plano, se você quiser usar o SSMS para simular um aplicativo enviando declarações preparadas, sp_prepare é seu amigo. (Veja também sp_prepexec, que pode ser um atalho se você não estiver tentando validar dois planos diferentes para a mesma consulta.)

Observe que este exercício não era necessariamente para obter um desempenho melhor o tempo todo – era para nivelar a variação de desempenho. As recompilações obviamente não são gratuitas, mas pagarei uma pequena penalidade para que 99% das minhas consultas sejam executadas em 250ms e 1% executadas em 5 segundos, em vez de ficar preso a um plano absolutamente terrível para 99% das consultas ou 1% das consultas.