Para encurtar a história, estamos atualizando pequenas tabelas de pessoas com valores de uma tabela muito grande de pessoas. Em um teste recente, esta atualização leva cerca de 5 minutos para ser executada.
Tropeçamos no que parece ser a otimização mais absurda possível, que aparentemente funciona perfeitamente! A mesma consulta agora é executada em menos de 2 minutos e produz os mesmos resultados, perfeitamente.
Aqui está a consulta. A última linha é adicionada como "a otimização". Por que a intensa diminuição no tempo de consulta? Estamos perdendo alguma coisa? Isso poderia levar a problemas no futuro?
UPDATE smallTbl
SET smallTbl.importantValue = largeTbl.importantValue
FROM smallTableOfPeople smallTbl
JOIN largeTableOfPeople largeTbl
ON largeTbl.birth_date = smallTbl.birthDate
AND DIFFERENCE(TRIM(smallTbl.last_name),TRIM(largeTbl.last_name)) = 4
AND DIFFERENCE(TRIM(smallTbl.first_name),TRIM(largeTbl.first_name)) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
AND LEFT(TRIM(largeTbl.last_name), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å')
Notas técnicas: Estamos cientes de que a lista de letras para testar pode precisar de mais algumas letras. Também estamos cientes da margem óbvia de erro ao usar "DIFERENÇA".
Plano de consulta (regular): https://www.brentozar.com/pastetheplan/?id=rypV84y7V
Plano de consulta (com "otimização"): https://www.brentozar.com/pastetheplan/?id=r1aC2my7E
AND LEFT(TRIM(largeTbl.last_name), 1) BETWEEN 'a' AND 'z' COLLATE LATIN1_GENERAL_CI_AI
faça o que quiser lá, sem exigir que você liste todos os caracteres e tenha um código difícil de lerWHERE
é falsa? Observe, em particular, que a comparação pode fazer distinção entre maiúsculas e minúsculas.Latin1_General_100_CI_AI
. E para o SQL Server 2012 e versões mais recentes (até pelo menos o SQL Server 2019), é melhor usar os agrupamentos ativados por Caracteres Suplementares na versão mais alta para o código do idioma que está sendo usado. Então isso seriaLatin1_General_100_CI_AI_SC
neste caso. Versões> 100 (somente japonês até agora) não possuem (ou precisam)_SC
(por exemploJapanese_XJIS_140_CI_AI
).Respostas:
Depende dos dados em suas tabelas, índices, ... Difícil dizer sem poder comparar os planos de execução / as estatísticas de tempo io +.
A diferença que eu esperaria é a filtragem extra acontecendo antes do JOIN entre as duas tabelas. No meu exemplo, alterei as atualizações para seleções para reutilizar minhas tabelas.
O plano de execução com "a otimização"
Plano de execução
Você vê claramente uma operação de filtro acontecendo. Nos meus dados de teste, nenhum registro foi filtrado e, como resultado, nenhuma melhoria foi feita.
O plano de execução, sem "a otimização"
Plano de execução
O filtro acabou, o que significa que teremos que confiar na junção para filtrar registros desnecessários.
Outro motivo (s) Outro motivo / consequência da alteração da consulta pode ser o fato de um novo plano de execução ter sido criado ao alterar a consulta, o que é mais rápido. Um exemplo disso é o mecanismo que escolhe um operador Join diferente, mas isso é apenas uma suposição neste momento.
EDITAR:
Esclarecendo depois de obter os dois planos de consulta:
A consulta está lendo Linhas de 550M da tabela grande e filtrando-as.
Significando que o predicado é quem realiza a maior parte da filtragem, não o predicado de busca. Resultando na leitura dos dados, mas muito menos sendo retornados.
Fazer o servidor sql usar um índice diferente (plano de consulta) / adicionar um índice pode resolver isso.
Então, por que a consulta de otimização não tem esse mesmo problema?
Como um plano de consulta diferente é usado, com uma varredura em vez de uma busca.
Sem fazer nenhuma busca, mas retornando apenas 4 milhões de linhas para trabalhar.
Próxima diferença
Desconsiderando a diferença de atualização (nada está sendo atualizado na consulta otimizada), uma correspondência de hash é usada na consulta otimizada:
Em vez de uma junção de loop aninhado no não otimizado:
Um loop aninhado é melhor quando uma tabela é pequena e a outra grande. Como os dois são do mesmo tamanho, eu argumentaria que a combinação de hash é a melhor escolha nesse caso.
visão global
A consulta otimizada
O plano da consulta otimizada tem paralelismo, usa uma junção de combinação de hash e precisa fazer menos filtragem de E / S residual. Ele também usa um bitmap para eliminar valores-chave que não podem produzir nenhuma linha de junção. (Também nada está sendo atualizado)
A consulta não otimizada O plano da consulta não otimizada não tem paralelismo, usa uma junção de loop aninhada e precisa fazer a filtragem de E / S residual em registros de 550 milhões. (Também a atualização está acontecendo)
O que você poderia fazer para melhorar a consulta não otimizada?
Alterando o índice para ter first_name e last_name na lista de colunas-chave:
CREATE INDEX IX_largeTableOfPeople_birth_date_first_name_last_name em dbo.largeTableOfPeople (data de nascimento, nome e sobrenome) include (id)
Porém, devido ao uso de funções e a tabela ser grande, essa pode não ser a solução ideal.
(HASH JOIN, MERGE JOIN)
à consultaDados de teste + consultas usadas
fonte
Não está claro que a segunda consulta seja de fato uma melhoria.
Os planos de execução contêm QueryTimeStats que mostram uma diferença muito menos dramática do que a declarada na pergunta.
O plano lento teve um tempo decorrido de
257,556 ms
(4 minutos e 17 segundos). O plano rápido teve um tempo decorrido de190,992 ms
(3 minutos e 11 segundos), apesar da execução com um grau de paralelismo de 3.Além disso, o segundo plano estava sendo executado em um banco de dados onde não havia trabalho a ser feito após a associação.
Primeiro plano
Segundo plano
Para que o tempo extra pudesse ser explicado pelo trabalho necessário para atualizar 3,5 milhões de linhas (o trabalho necessário no operador de atualização para localizar essas linhas, trancar a página, gravar a atualização na página e o log de transações não é desprezível)
Se isso é de fato reproduzível quando se compara o like com o like, então a explicação é que você teve sorte nesse caso.
O filtro com as 37
IN
condições eliminou apenas 51 linhas das 4.008.334 na tabela, mas o otimizador considerou que eliminaria muito maisTais estimativas incorretas de cardinalidade são geralmente uma coisa ruim. Nesse caso, ele produziu um plano de forma diferente (e paralelo) que aparentemente (?) Funcionou melhor para você, apesar dos derramamentos de hash causados pela enorme subestimação.
Sem o
TRIM
SQL Server, é possível convertê-lo em um intervalo de intervalo no histograma da coluna base e fornecer estimativas muito mais precisas, mas com oTRIM
recurso apenas a adivinhações.A natureza do palpite pode variar, mas a estimativa para um único predicado
LEFT(TRIM(largeTbl.last_name), 1)
está em algumas circunstâncias * apenas estimadastable_cardinality/estimated_number_of_distinct_column_values
.Não sei exatamente em que circunstâncias - o tamanho dos dados parece desempenhar um papel. Consegui reproduzir isso com tipos de dados de comprimento fixo amplo, como aqui, mas obtive um palpite diferente e mais alto
varchar
(que apenas usou um palpite plano de 10% e estimou 100.000 linhas). @Solomon Rutzky salienta que, sevarchar(100)
for preenchido com espaços à direita, como acontece comchar
a estimativa mais baixa, é usadoA
IN
lista é expandidaOR
e o SQL Server usa backoff exponencial com um máximo de 4 predicados considerados. Portanto, a219.707
estimativa é alcançada da seguinte forma.fonte