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

O caso do Red Herring da estimativa de cardinalidade


Você está trabalhando com um desenvolvedor que está relatando um desempenho lento para a seguinte chamada de procedimento armazenado:
EXEC [dbo].[charge_by_date] '2/28/2013';

Você pergunta qual problema o desenvolvedor está vendo, mas a única informação adicional que você ouve de volta é que ele está "rodando lentamente". Então você salta para a instância do SQL Server e dá uma olhada no real plano de execução. Você faz isso porque está interessado não apenas na aparência do plano de execução, mas também no número estimado versus o número real de linhas para o plano:



Olhando primeiro apenas para as operadoras de planos, você pode ver alguns detalhes dignos de nota:
  • Há um aviso no operador raiz
  • Há uma verificação de tabela para ambas as tabelas referenciadas no nível folha (charge_jan e charge_feb) e você se pergunta por que ambas ainda são heaps e não têm índices clusterizados
  • Você vê que há apenas linhas passando pela tabela charge_feb e não pela tabela charge_jan
  • Você vê zonas paralelas no plano

Quanto ao aviso no iterador raiz, você passa o mouse sobre ele e vê que há avisos de índice ausentes com uma recomendação para os seguintes índices:
CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[charge_feb] ([charge_dt])
INCLUDE ([charge_no])
GO
 
CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[charge_jan] ([charge_dt])
INCLUDE ([charge_no])
GO

Você pergunta ao desenvolvedor do banco de dados original por que não há um índice clusterizado e a resposta é "Não sei".

Continuando a investigação antes de fazer qualquer alteração, você olha para a guia Plan Tree no SQL Sentry Plan Explorer e realmente vê que há desvios significativos entre as linhas estimadas e reais para uma das tabelas:



Parece haver dois problemas:
  • Uma subestimativa para linhas na verificação da tabela charge_jan
  • Uma superestimativa de linhas na verificação da tabela charge_feb

Portanto, as estimativas de cardinalidade são distorcido, e você se pergunta se isso está relacionado ao sniffing de parâmetros. Você decide verificar o valor compilado do parâmetro e compará-lo com o valor do tempo de execução do parâmetro, que você pode ver na guia Parâmetros:



Na verdade, existem diferenças entre o valor de tempo de execução e o valor compilado. Você copia o banco de dados para um ambiente de teste do tipo prod e, em seguida, testa a execução do procedimento armazenado com o valor de tempo de execução de 28/02/2013 primeiro e depois 31/01/2013.

Os planos de 28/02/2013 e 31/01/2013 têm formas idênticas, mas fluxos de dados reais diferentes. O plano de 28/02/2013 e as estimativas de cardinalidade foram os seguintes:





E enquanto o plano de 28/02/2013 não mostra nenhum problema de estimativa de cardinalidade, o plano de 31/01/2013:





Portanto, o segundo plano mostra as mesmas estimativas acima e abaixo, apenas revertidas do plano original que você olhou.

Você decide adicionar os índices sugeridos ao ambiente de teste do tipo prod para as tabelas charge_jan e charge_feb e ver se isso ajuda. Executando os procedimentos armazenados na ordem de janeiro/fevereiro, você vê as seguintes novas formas de plano e estimativas de cardinalidade associadas:









O novo plano usa uma operação Index Seek de cada tabela, mas você ainda vê zero linhas fluindo de uma tabela e não da outra, e ainda vê desvios de estimativa de cardinalidade com base na detecção de parâmetros quando o valor do tempo de execução está em um mês diferente da compilação valor do tempo.

Sua equipe tem uma política de não adicionar índices sem comprovação de benefício suficiente e testes de regressão associados. Você decide, por enquanto, remover os índices não clusterizados que acabou de criar. Embora você não resolva imediatamente o clustered ausente index, você decide que cuidará disso mais tarde.

Neste ponto, você percebe que precisa analisar mais a definição do procedimento armazenado, que é o seguinte:
CREATE PROCEDURE dbo.charge_by_date
  @charge_dt datetime
AS
  SELECT charge_no
  FROM dbo.charge_view
  WHERE charge_dt = @charge_dt
GO

Em seguida, você observa a definição do objeto charge_view:
CREATE VIEW charge_view
AS
  SELECT *
  FROM [charge_jan]
  UNION ALL
  SELECT *
  FROM [charge_feb]
GO

A exibição faz referência a dados de cobrança separados em diferentes tabelas por data. E então você se pergunta se a segunda distorção do plano de execução da consulta pode ser evitada alterando a definição do procedimento armazenado.

Talvez se o otimizador souber em tempo de execução qual é o valor, o problema da estimativa de cardinalidade desaparecerá e melhorará o desempenho geral?

Você vai em frente e redefine a chamada do procedimento armazenado da seguinte forma, adicionando uma dica RECOMPILE (sabe que você também ouviu que isso pode aumentar o uso da CPU, mas como este é um ambiente de teste, você se sente seguro em tentar):
ALTER PROCEDURE charge_by_date
  @charge_dt datetime
AS
  SELECT charge_no
  FROM dbo.charge_view
  WHERE charge_dt = @charge_dt
  OPTION (RECOMPILE);
GO

Em seguida, execute novamente o procedimento armazenado usando o valor de 31/01/2013 e, em seguida, o valor de 28/02/2013.

A forma do plano permanece a mesma, mas agora o problema de estimativa de cardinalidade foi removido.

Os dados da estimativa de cardinalidade de 31/01/2013 mostram:



E os dados da estimativa de cardinalidade de 28/02/2013 mostram:



Isso deixa você feliz por um momento, mas depois percebe que a duração da execução geral da consulta parece relativamente a mesma de antes. Você começa a ter dúvidas de que o desenvolvedor ficará feliz com seus resultados. Você resolveu a distorção da estimativa de cardinalidade, mas sem o aumento de desempenho esperado, não tem certeza se ajudou de alguma forma significativa.

É nesse ponto que você percebe que o plano de execução da consulta é apenas um subconjunto das informações de que pode precisar e, portanto, expande ainda mais sua exploração examinando a guia Table I/O. Você vê a seguinte saída para a execução de 31/01/2013:



E para a execução de 28/02/2013 você vê dados semelhantes:



É nesse ponto que você se pergunta se as operações de acesso a dados para ambos tabelas são necessárias em cada plano. Se o otimizador sabe que você só precisa de linhas de janeiro, por que acessar fevereiro e vice-versa? Você também se lembra de que o otimizador de consultas não tem garantias de que não haja linhas reais dos outros meses na tabela "errada", a menos que tais garantias tenham sido feitas explicitamente por meio de restrições na própria tabela.

Você verifica as definições de tabela por meio de sp_help para cada tabela e não vê nenhuma restrição definida para nenhuma tabela.

Então, como teste, você adiciona as duas restrições a seguir:
ALTER TABLE [dbo].[charge_jan]
  ADD CONSTRAINT charge_jan_chk CHECK
  (charge_dt >= '1/1/2013' AND charge_dt < '2/1/2013');
GO
 
ALTER TABLE [dbo].[charge_feb]
  ADD CONSTRAINT charge_feb_chk CHECK
  (charge_dt >= '2/1/2013' AND charge_dt < '3/1/2013');
GO

Você reexecuta os procedimentos armazenados e vê as seguintes formas de plano e estimativas de cardinalidade.

Execução em 31/01/2013:





Execução em 28/02/2013:





Observando a Tabela I/O novamente, você vê a seguinte saída para a execução de 31/01/2013:



E para a execução de 28/02/2013 você vê dados semelhantes, mas para a tabela charge_feb:



Lembrando que você ainda tem o RECOMPILE na definição do procedimento armazenado, tente removê-lo e veja se vê o mesmo efeito. Depois de fazer isso, você verá o retorno de acesso de duas tabelas, mas sem leituras lógicas reais para a tabela que não possui linhas (em comparação com o plano original sem as restrições). Por exemplo, a execução de 31/01/2013 mostrou a seguinte saída de E/S da Tabela:



Você decide avançar com o teste de carga das novas restrições CHECK e solução RECOMPILE, removendo o acesso à tabela inteiramente do plano (e dos operadores de plano associados). Você também se prepara para um debate sobre a chave de índice clusterizado e um índice não clusterizado de suporte adequado que acomodará um conjunto mais amplo de cargas de trabalho que atualmente acessam as tabelas associadas.