Eu tenho uma visão muito importante e muito lenta, que inclui algumas condições realmente feias como esta na cláusula where. Também estou ciente de que as junções são brutas e lentas em varchar(13)
vez de campos de identidade inteira, mas gostaria de melhorar a consulta simples abaixo que usa esta exibição:
CREATE VIEW [dbo].[vwReallySlowView] AS
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.tblItemtran AS I
INNER JOIN -- booking_no = varchar(13)
dbo.tblbookings AS B ON B.booking_no = I.booking_no_v32 -- string inner-join
INNER JOIN -- product_code = varchar(13)
dbo.tblInvmas AS M ON I.product_code_v42 = M.product_code -- string inner-join
WHERE (I.trans_type_v41 NOT IN (2, 3, 7, 18, 19, 20, 21, 12, 13, 22)) AND (I.trans_type_v41 NOT IN (6, 7)) AND (I.bit_field_v41 & 4 = 0) OR
(I.trans_type_v41 NOT IN (6, 7)) AND (I.bit_field_v41 & 4 = 0) AND (B.BookingProgressStatus = 1) OR
(I.trans_type_v41 IN (6, 7)) AND (I.bit_field_v41 & 4 = 0) AND (I.QtyCheckedOut = 0) OR
(I.trans_type_v41 IN (6, 7)) AND (I.bit_field_v41 & 4 = 0) AND (I.QtyCheckedOut > 0) AND (I.trans_qty - (I.QtyCheckedOut - I.QtyReturned) > 0)
Essa visualização geralmente é usada assim:
select * from vwReallySlowView
where product_code_v42 = 'LIGHTBULB100W' -- find "100 watt lightbulb" rows
Quando eu executá-lo eu recebo este item plano de execução custando 20 a 80% do custo total do lote, com predicado CONVERT_IMPLICIT( .... &(4))
mostrando que parece ser muito lento em fazer estes bitwise boolean tests
como (I.ibitfield & 4 = 0)
.
Não sou especialista em MS SQL ou no trabalho do tipo DBA em geral, pois na maioria das vezes sou desenvolvedor de software não-SQL. Mas suspeito que essas combinações bit a bit são uma péssima idéia e que seria melhor ter campos booleanos discretos.
De alguma forma, poderia melhorar esse índice que tenho, para lidar melhor com essa visualização sem alterar o esquema (que já está em produção em milhares de locais) ou devo alterar a tabela subjacente que possui vários valores booleanos compactados em um número inteiro bit_field_v41
, para corrigir esse problema ?
Aqui está o meu Índice de Cluster no tblItemtran
qual está sendo verificado neste plano de execução:
-- goal: speed up select * from vwReallySlowView where productcode = 'X'
CREATE CLUSTERED INDEX [idxtblItemTranProductCodeAndTransType] ON [dbo].[tblItemtran]
(
[product_code_v42] ASC, -- varchar(13)
[trans_type_v41] ASC -- int
)WITH ( PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF,
IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON)
ON [PRIMARY]
Aqui está o plano de execução, para um dos outros produtos que resulta em um custo de 27% nesse CONVERT_IMPLICIT
predicado. update Observe que, neste caso, meu pior nó agora é "combinação de hash" em um inner join
, que custa 34%. Acredito que esse é um custo que não posso evitar, a menos que evite fazer junções em strings que atualmente não posso livrar-se de. Ambas as INNER JOIN
operações na exibição acima estão nos varchar(13)
campos.
Ampliado no canto inferior direito:
Todo o plano de execução como .sqlplan está disponível no skydrive. Esta imagem é apenas uma visão geral. Clique aqui para ver a imagem por si só.
A atualização postou todo o plano de execução. Parece que não consigo descobrir qual o product_code
valor patologicamente ruim, mas uma maneira de fazer isso é em select count(*) from view
vez de criar um único produto. Mas os produtos que são usados apenas em 5% dos registros da tabela subjacente ou menos parecem mostrar custos muito mais baixos na CONVERT_IMPLICIT
operação. Se eu fosse consertar o SQL aqui, acho que pegaria a WHERE
cláusula bruta na exibição e calcularia e armazenaria o resultado dessa condição de cláusula where gigante como um campo de bits "IncludeMeInTheView", na tabela subjacente . Presto, problema resolvido, certo?
fonte
product_code
valor que usei para gerar esse87%
caso patológico com meus dados. As imagens agora mostram27%
. Mais uma vez, peço desculpas pela confusão devido às minhas edições.Respostas:
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 residualbit_field_v41 & 4 = 0
aplicado. Há uma conversão implícita debit_field_v41
do tipo base (tinyint
ousmallint
) parainteger
.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ãobit_field_v41
houvertinyint
nenhuma conversão, ocorrerá. Da mesma forma,CONVERT(smallint, 4)
poderia ser usado sebit_field_v41
forsmallint
. 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):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 depoisDBCC 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: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):
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 abooking_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: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 .
A visualização existente foi ajustada para usar a visualização indexada acima:
Exemplo de consulta e plano de execução:
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.
fonte