Por que isso é mais rápido e seguro de usar? (ONDE a primeira letra está no alfabeto)

10

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

JohnF
fonte
4
Pequena resposta à sua nota técnica: AND LEFT(TRIM(largeTbl.last_name), 1) BETWEEN 'a' AND 'z' COLLATE LATIN1_GENERAL_CI_AIfaça o que quiser lá, sem exigir que você liste todos os caracteres e tenha um código difícil de ler
Erik A
Você tem linhas em que a condição final em WHEREé falsa? Observe, em particular, que a comparação pode fazer distinção entre maiúsculas e minúsculas.
Jpmc26 18/01/19
@ErikvonAsmuth faz uma excelente observação. Mas, apenas uma pequena nota técnica: para o SQL Server 2008 e 2008 R2, é melhor usar os agrupamentos da versão "100" (se disponíveis para a cultura / localidade em uso). Então seria isso 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 seria Latin1_General_100_CI_AI_SCneste caso. Versões> 100 (somente japonês até agora) não possuem (ou precisam) _SC(por exemplo Japanese_XJIS_140_CI_AI).
Solomon Rutzky 18/01/19

Respostas:

9

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" insira a descrição da imagem aqui

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" insira a descrição da imagem aqui

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. insira a descrição da imagem aqui

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.

insira a descrição da imagem aqui insira a descrição da imagem aqui

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:

insira a descrição da imagem aqui

Em vez de uma junção de loop aninhado no não otimizado:

insira a descrição da imagem aqui

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 insira a descrição da imagem aqui

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 insira a descrição da imagem aqui 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.

  • Atualizando estatísticas, usando recompilar para tentar obter o melhor plano.
  • Adicionando OPTION (HASH JOIN, MERGE JOIN)à consulta
  • ...

Dados de teste + consultas usadas

CREATE TABLE #smallTableOfPeople(importantValue int, birthDate datetime2, first_name varchar(50),last_name varchar(50));
CREATE TABLE #largeTableOfPeople(importantValue int, birth_date datetime2, first_name varchar(50),last_name varchar(50));


set nocount on;
DECLARE @i int = 1
WHILE @i <= 1000
BEGIN
insert into #smallTableOfPeople (importantValue,birthDate,first_name,last_name)
VALUES(NULL, dateadd(mi,@i,'2018-01-18 11:05:29.067'),'Frodo','Baggins');

set @i += 1;
END


set nocount on;
DECLARE @j int = 1
WHILE @j <= 20000
BEGIN
insert into #largeTableOfPeople (importantValue,birth_Date,first_name,last_name)
VALUES(@j, dateadd(mi,@j,'2018-01-18 11:05:29.067'),'Frodo','Baggins');

set @j += 1;
END


SET STATISTICS IO, TIME ON;

SELECT  smallTbl.importantValue , largeTbl.importantValue
FROM #smallTableOfPeople smallTbl
JOIN #largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.last_name)),RTRIM(LTRIM(largeTbl.last_name))) = 4
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.first_name)),RTRIM(LTRIM(largeTbl.first_name))) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
AND LEFT(RTRIM(LTRIM(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','æ','ä','ø','å');

SELECT  smallTbl.importantValue , largeTbl.importantValue
FROM #smallTableOfPeople smallTbl
JOIN #largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.last_name)),RTRIM(LTRIM(largeTbl.last_name))) = 4
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.first_name)),RTRIM(LTRIM(largeTbl.first_name))) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
--AND LEFT(RTRIM(LTRIM(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','æ','ä','ø','å')




drop table #largeTableOfPeople;
drop table #smallTableOfPeople;
Randi Vertongen
fonte
8

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 de 190,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

insira a descrição da imagem aqui

Segundo plano

insira a descrição da imagem aqui

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 INcondições eliminou apenas 51 linhas das 4.008.334 na tabela, mas o otimizador considerou que eliminaria muito mais

insira a descrição da imagem aqui

   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', 'æ', 'ä', 'ø', 'å' ) 

Tais 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 TRIMSQL Server, é possível convertê-lo em um intervalo de intervalo no histograma da coluna base e fornecer estimativas muito mais precisas, mas com o TRIMrecurso 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 estimadas table_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, se varchar(100)for preenchido com espaços à direita, como acontece com chara estimativa mais baixa, é usado

A INlista é expandida ORe o SQL Server usa backoff exponencial com um máximo de 4 predicados considerados. Portanto, a 219.707estimativa é alcançada da seguinte forma.

DECLARE @TableCardinality FLOAT = 4008334, 
        @DistinctColumnValueEstimate FLOAT = 34207

DECLARE @NotSelectivity float = 1 - (1/@DistinctColumnValueEstimate)

SELECT @TableCardinality * ( 1 - (
@NotSelectivity * 
SQRT(@NotSelectivity) * 
SQRT(SQRT(@NotSelectivity)) * 
SQRT(SQRT(SQRT(@NotSelectivity)))
))
Martin Smith
fonte