Você não deve confiar muito nas porcentagens de custo nos planos de execução. Esses sempre são custos estimados , mesmo em planos pós-execução com números 'reais' para itens como contagem de linhas. Os custos estimados são baseados em um modelo que funciona muito bem para a finalidade a que se destina: permitir que o otimizador escolha entre diferentes planos de execução de candidatos para a mesma consulta. As informações de custo são interessantes e um fator a ser considerado, mas raramente deve ser uma métrica principal para o ajuste de consultas. A interpretação das informações do plano de execução requer uma visão mais ampla dos dados apresentados.
Operador de busca de índice clusterizado ItemTran
Este operador é realmente duas operações em uma. Primeiro, uma operação de busca de índice localiza todas as linhas que correspondem ao predicado e product_code_v42 = 'M10BOLT'
, em seguida, cada linha tem o predicado residual bit_field_v41 & 4 = 0
aplicado. Há uma conversão implícita de bit_field_v41
do tipo base ( tinyint
ou smallint
) para integer
.
A conversão ocorre porque o operador AND bit a bit (&) requer que ambos os operandos sejam do mesmo tipo. O tipo implícito do valor constante '4' é inteiro e as regras de precedência do tipo de dados significam que o bit_field_v41
valor do campo de prioridade mais baixa é convertido.
O problema (como ele é) é facilmente corrigido escrevendo o predicado como bit_field_v41 & CONVERT(tinyint, 4) = 0
- o que significa que o valor constante tem a menor prioridade e é convertido (durante a dobragem constante) em vez do valor da coluna. Se não bit_field_v41
houver tinyint
nenhuma conversão, ocorrerá. Da mesma forma, CONVERT(smallint, 4)
poderia ser usado se bit_field_v41
for smallint
. Dito isso, a conversão não é um problema de desempenho nesse caso, mas ainda é uma boa prática corresponder tipos e evitar conversões implícitas sempre que possível.
A maior parte do custo estimado dessa busca está no tamanho da tabela base. Enquanto a chave de índice em cluster é razoavelmente estreita, o tamanho de cada linha é grande. Uma definição para a tabela não é fornecida, mas apenas as colunas usadas na exibição somam uma largura de linha significativa. Como o índice em cluster inclui todas as colunas, a distância entre as chaves de índice em cluster é a largura da linha , não a largura das chaves de índice . O uso de sufixos de versão em algumas colunas sugere que a tabela real possui ainda mais colunas para versões anteriores.
Observando as colunas de busca, predicado residual e saída, o desempenho desse operador pode ser verificado isoladamente através da criação de uma consulta equivalente (esse 1 <> 2
é um truque para evitar a parametrização automática, a contradição é removida pelo otimizador e não aparece no plano de consulta):
SELECT
it.booking_no_v32,
it.QtyCheckedOut,
it.QtyReturned,
it.Trans_qty,
it.trans_type_v41
FROM dbo.tblItemTran AS it
WHERE
1 <> 2
AND it.product_code_v42 = 'M10BOLT'
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0;
O desempenho desta consulta com um cache de dados frio é interessante, pois a leitura antecipada seria afetada pela fragmentação da tabela (índice clusterizado). A chave de cluster para esta tabela convida à fragmentação; portanto, pode ser importante manter (reorganizar ou reconstruir) esse índice regularmente e usar um apropriado FILLFACTOR
para permitir espaço para novas linhas entre as janelas de manutenção de índice.
Eu realizei um teste do efeito da fragmentação na leitura antecipada usando dados de amostra gerados usando o SQL Data Generator . Usando as mesmas contagens de linhas da tabela, como mostrado no plano de consulta da pergunta, um índice em cluster altamente fragmentado resultou em SELECT * FROM view
15 segundos depois DBCC DROPCLEANBUFFERS
. O mesmo teste nas mesmas condições com um índice de cluster recém-reconstruído na tabela ItemTrans concluído em 3 segundos.
Se os dados da tabela geralmente estão inteiramente no cache, o problema de fragmentação é muito menos importante. Mas, mesmo com baixa fragmentação, as linhas largas da tabela podem significar que o número de leituras lógicas e físicas é muito maior do que o esperado. Você também pode experimentar adicionar e remover o explícito CONVERT
para validar minha expectativa de que o problema implícito de conversão não seja importante aqui, exceto como uma violação das práticas recomendadas.
Mais importante é o número estimado de linhas saindo do operador de busca. A estimativa do tempo de otimização é de 165 linhas, mas 4.226 foram produzidas no tempo de execução. Voltarei a esse ponto mais tarde, mas a principal razão da discrepância é que a seletividade do predicado residual (envolvendo o AND bit a bit) é muito difícil para o otimizador prever - na verdade, ele recorre à adivinhação.
Operador de filtro
Estou mostrando o predicado de filtro aqui principalmente para ilustrar como as duas NOT IN
listas são combinadas, simplificadas e depois expandidas, e também para fornecer uma referência para a seguinte discussão de correspondência de hash. A consulta de teste da pesquisa pode ser expandida para incorporar seus efeitos e determinar o efeito do operador Filtro no desempenho:
SELECT
it.booking_no_v32,
it.trans_type_v41,
it.Trans_qty,
it.QtyReturned,
it.QtyCheckedOut
FROM dbo.tblItemTran AS it
WHERE
it.product_code_v42 = 'M10BOLT'
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
AND
(
(
it.trans_type_v41 NOT IN (2, 3, 6, 7, 18, 19, 20, 21, 12, 13, 22)
AND it.trans_type_v41 NOT IN (6, 7)
)
OR
(
it.trans_type_v41 NOT IN (6, 7)
)
OR
(
it.trans_type_v41 IN (6, 7)
AND it.QtyCheckedOut = 0
)
OR
(
it.trans_type_v41 IN (6, 7)
AND it.QtyCheckedOut > 0
AND it.trans_qty - (it.QtyCheckedOut - it.QtyReturned) > 0
)
);
O operador Compute Scalar no plano define a seguinte expressão (o próprio cálculo é adiado até que o resultado seja requerido por um operador posterior):
[Expr1016] = (trans_qty - (QtyCheckedOut - QtyReturned))
O operador Hash Match
A realização de uma junção nos tipos de dados de caracteres não é a razão do alto custo estimado desse operador. A dica de ferramenta do SSMS mostra apenas uma entrada do Hash Keys Probe, mas os detalhes importantes estão na janela Propriedades do SSMS.
O operador Hash Match cria uma tabela de hash usando os valores da booking_no_v32
coluna (Hash Keys Build) da tabela ItemTran e, em seguida, procura por correspondências usando a booking_no
coluna (Hash Keys Probe) da tabela Bookings. A dica de ferramenta do SSMS também normalmente mostra um Residual do probe, mas o texto é muito longo para uma dica de ferramenta e é simplesmente omitido.
Um Sonda Residual é semelhante ao Residual visto após a busca do índice anteriormente; o predicado residual é avaliado em todas as linhas que correspondem a hash para determinar se a linha deve ser passada para o operador pai. A localização de correspondências de hash em uma tabela de hash bem equilibrada é extremamente rápida, mas a aplicação de um predicado residual complexo a cada linha correspondente é bastante lenta em comparação. A dica de ferramenta Hash Match no Plan Explorer mostra os detalhes, incluindo a expressão Residual do probe:
O predicado residual é complexo e inclui a verificação do status do progresso da reserva agora que a coluna está disponível na tabela de reservas. A dica de ferramenta também mostra a mesma discrepância entre as contagens estimadas e reais de linhas vistas anteriormente na busca do índice. Pode parecer estranho que grande parte da filtragem seja realizada duas vezes, mas esse é apenas o otimizador otimista. Ele não espera que as partes do filtro que podem ser empurradas para baixo do plano do resíduo da sonda elimine todas as linhas (as estimativas de contagem de linhas são as mesmas antes e depois do filtro), mas o otimizador sabe que pode estar errado sobre isso. A chance de filtrar linhas antecipadamente (reduzindo o custo da junção de hash) vale o pequeno custo do filtro extra. O filtro inteiro não pode ser pressionado para baixo porque inclui um teste em uma coluna da tabela de reservas, mas a maioria pode ser.
A subestimação da contagem de linhas é um problema para o operador Hash Match porque a quantidade de memória reservada para a tabela de hash é baseada no número estimado de linhas. Onde a memória é muito pequena para o tamanho da tabela de hash necessária no tempo de execução (devido ao maior número de linhas), a tabela de hash se derrama recursivamente para o armazenamento físico do tempdb , geralmente resultando em um desempenho muito ruim. Na pior das hipóteses, o mecanismo de execução para de recursivamente derramar baldes de hash e recorre a um processo muito lento.algoritmo de resgate. O derramamento de hash (recursivo ou resgate) é a causa mais provável dos problemas de desempenho descritos na pergunta (não colunas de junção do tipo caractere ou conversões implícitas). A causa raiz seria o servidor reservando pouca memória para a consulta com base na estimativa incorreta da contagem de linhas (cardinalidade).
Infelizmente, antes do SQL Server 2012, não há indicação no plano de execução de que uma operação de hash excedeu sua alocação de memória (que não pode crescer dinamicamente depois de ser reservada antes do início da execução, mesmo que o servidor tenha grandes quantidades de memória livre) e precisou se derramar para tempdb. É possível monitorar a classe de eventos de aviso de hash usando o Profiler, mas pode ser difícil correlacionar os avisos com uma consulta específica.
Corrigindo os problemas
Os três problemas são a fragmentação, a sonda complexa residual no operador de combinação de hash e a estimativa de cardinalidade incorreta resultante da tentativa de adivinhação na busca do índice.
Solução recomendada
Verifique a fragmentação e corrija-a, se necessário, agendando a manutenção para garantir que o índice permaneça organizado de maneira aceitável. A maneira usual de corrigir a estimativa de cardinalidade é fornecer estatísticas. Nesse caso, o otimizador precisa de estatísticas para a combinação ( product_code_v42
, bitfield_v41 & 4 = 0
). Como não podemos criar estatísticas diretamente em uma expressão, devemos primeiro criar uma coluna computada para a expressão do campo de bits e, em seguida, criar as estatísticas manuais de várias colunas:
ALTER TABLE dbo.tblItemTran
ADD Bit3 AS bit_field_v41 & CONVERT(tinyint, 4);
CREATE STATISTICS [stats dbo.ItemTran (product_code_v42, Bit3)]
ON dbo.tblItemTran (product_code_v42, Bit3);
A definição de texto da coluna calculada deve corresponder exatamente ao texto na definição de visualização para que as estatísticas sejam usadas, portanto, a correção da visualização para eliminar a conversão implícita deve ser feita ao mesmo tempo e deve-se tomar cuidado para garantir uma correspondência textual.
As estatísticas de várias colunas devem resultar em estimativas muito melhores, reduzindo bastante a chance de o operador de combinação de hash usar derramamento recursivo ou o algoritmo de resgate. Adicionar a coluna computada (que é uma operação apenas de metadados e não ocupa espaço na tabela, pois não está marcada PERSISTED
) e as estatísticas de várias colunas são meu melhor palpite para uma primeira solução.
Ao resolver problemas de desempenho da consulta, é importante medir coisas como tempo decorrido, uso da CPU, leituras lógicas, leituras físicas, tipos de espera e durações ... e assim por diante. Também pode ser útil executar partes da consulta separadamente para validar as causas suspeitas, como mostrado acima.
Em alguns ambientes, onde uma visualização atualizada dos dados não é importante, pode ser útil executar um processo em segundo plano que materialize toda a visualização em uma tabela de instantâneos de vez em quando. Esta tabela é apenas uma tabela base normal e pode ser indexada para consultas de leitura sem se preocupar com o desempenho da atualização.
Exibir indexação
Não fique tentado a indexar diretamente a exibição original. O desempenho da leitura será incrivelmente rápido (uma única busca em um índice de exibição), mas (nesse caso) todos os problemas de desempenho nos planos de consulta existentes serão transferidos para consultas que modificam qualquer uma das colunas da tabela mencionada na exibição. As consultas que alteram as linhas da tabela base serão muito afetadas.
Solução avançada com uma exibição indexada parcial
Existe uma solução de exibição indexada parcial para essa consulta específica que corrige as estimativas de cardinalidade e remove o filtro e a sonda residual, mas ela é baseada em algumas suposições sobre os dados (principalmente meu palpite no esquema) e requer implementação especializada, particularmente no que se refere a índices para suportar os planos de manutenção de exibição indexada. Eu compartilho o código abaixo por interesse, não proponho que você o implemente sem análises e testes muito cuidadosos .
-- Indexed view to optimize the main view
CREATE VIEW dbo.V1
WITH SCHEMABINDING
AS
SELECT
it.ID,
it.product_code_v42,
it.trans_type_v41,
it.booking_no_v32,
it.Trans_qty,
it.QtyReturned,
it.QtyCheckedOut,
it.QtyReserved,
it.bit_field_v41,
it.prep_on,
it.From_locn,
it.Trans_to_locn,
it.PDate,
it.FirstDate,
it.PTimeH,
it.PTimeM,
it.RetnDate,
it.BookDate,
it.TimeBookedH,
it.TimeBookedM,
it.TimeBookedS,
it.del_time_hour,
it.del_time_min,
it.return_to_locn,
it.return_time_hour,
it.return_time_min,
it.AssignTo,
it.AssignType,
it.InRack
FROM dbo.tblItemTran AS it
JOIN dbo.tblBookings AS tb ON
tb.booking_no = it.booking_no_v32
WHERE
(
it.trans_type_v41 NOT IN (2, 3, 7, 18, 19, 20, 21, 12, 13, 22)
AND it.trans_type_v41 NOT IN (6, 7)
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
)
OR
(
it.trans_type_v41 NOT IN (6, 7)
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
AND tb.BookingProgressStatus = 1
)
OR
(
it.trans_type_v41 IN (6, 7)
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
AND it.QtyCheckedOut = 0
)
OR
(
it.trans_type_v41 IN (6, 7)
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
AND it.QtyCheckedOut > 0
AND it.trans_qty - (it.QtyCheckedOut - it.QtyReturned) > 0
);
GO
CREATE UNIQUE CLUSTERED INDEX cuq ON dbo.V1 (product_code_v42, ID);
GO
A visualização existente foi ajustada para usar a visualização indexada acima:
CREATE VIEW [dbo].[vwReallySlowView2]
AS
SELECT
I.booking_no_v32 AS bkno,
I.trans_type_v41 AS trantype,
B.Assigned_to_v61 AS Assignbk,
B.order_date AS dateo,
B.HourBooked AS HBooked,
B.MinBooked AS MBooked,
B.SecBooked AS SBooked,
I.prep_on AS Pon,
I.From_locn AS Flocn,
I.Trans_to_locn AS TTlocn,
CASE I.prep_on
WHEN 'Y' THEN I.PDate
ELSE I.FirstDate
END AS PrDate,
I.PTimeH AS PrTimeH,
I.PTimeM AS PrTimeM,
CASE
WHEN I.RetnDate < I.FirstDate
THEN I.FirstDate
ELSE I.RetnDate
END AS RDatev,
I.bit_field_v41 AS bitField,
I.FirstDate AS FDatev,
I.BookDate AS DBooked,
I.TimeBookedH AS TBookH,
I.TimeBookedM AS TBookM,
I.TimeBookedS AS TBookS,
I.del_time_hour AS dth,
I.del_time_min AS dtm,
I.return_to_locn AS rtlocn,
I.return_time_hour AS rth,
I.return_time_min AS rtm,
CASE
WHEN
I.Trans_type_v41 IN (6, 7)
AND I.Trans_qty < I.QtyCheckedOut
THEN 0
WHEN
I.Trans_type_v41 IN (6, 7)
AND I.Trans_qty >= I.QtyCheckedOut
THEN I.Trans_Qty - I.QtyCheckedOut
ELSE
I.trans_qty
END AS trqty,
CASE
WHEN I.Trans_type_v41 IN (6, 7)
THEN 0
ELSE I.QtyCheckedOut
END AS MyQtycheckedout,
CASE
WHEN I.Trans_type_v41 IN (6, 7)
THEN 0
ELSE I.QtyReturned
END AS retqty,
I.ID,
B.BookingProgressStatus AS bkProg,
I.product_code_v42,
I.return_to_locn,
I.AssignTo,
I.AssignType,
I.QtyReserved,
B.DeprepOn,
CASE B.DeprepOn
WHEN 1 THEN B.DeprepDateTime
ELSE I.RetnDate
END AS DeprepDateTime,
I.InRack
FROM dbo.V1 AS I WITH (NOEXPAND)
JOIN dbo.tblbookings AS B ON
B.booking_no = I.booking_no_v32
JOIN dbo.tblInvmas AS M ON
I.product_code_v42 = M.product_code;
Exemplo de consulta e plano de execução:
SELECT
vrsv.*
FROM dbo.vwReallySlowView2 AS vrsv
WHERE vrsv.product_code_v42 = 'M10BOLT';
No novo plano, a correspondência de hash não tem predicado residual , não há filtro complexo , predicado residual na busca de exibição indexada e as estimativas de cardinalidade estão exatamente corretas.
Como um exemplo de como os planos de inserção / atualização / exclusão seriam afetados, este é o plano para uma inserção na tabela ItemTrans:
A seção destacada é nova e necessária para a manutenção da exibição indexada. O spool de tabela substitui as linhas de tabela base inseridas para manutenção de exibição indexada. Cada linha é associada à tabela de reservas usando uma busca de índice em cluster e, em seguida, um filtro aplica os WHERE
predicados de cláusula complexa para ver se a linha precisa ser adicionada à visualização. Nesse caso, uma inserção é executada no índice de cluster da exibição.
O mesmo SELECT * FROM view
teste realizado anteriormente foi concluído em 150ms com a exibição indexada no lugar.
Final: notei que seu servidor 2008 R2 ainda está na RTM. Ele não solucionará seus problemas de desempenho, mas o Service Pack 2 para 2008 R2 está disponível desde julho de 2012 e há muitos bons motivos para manter o mais atualizado possível com os service packs.