Enorme desaceleração na consulta do SQL Server ao adicionar curinga (ou superior)

52

Eu tenho um zoológico de 20 milhões de animais que acompanho no meu banco de dados do SQL Server 2005. Cerca de 1% deles são pretos e cerca de 1% deles são cisnes. Eu queria obter detalhes de todos os cisnes negros e, por isso, não queria invadir a página de resultados que fiz:

select top 10 * 
from animal 
where colour like 'black'  
and species like 'swan'

(Sim, não é recomendável que esses campos sejam de texto livre, mas ambos são indexados). Acontece que não temos animais, pois a consulta retorna um conjunto vazio em cerca de 300 milissegundos. Teria sido duas vezes mais rápido se eu tivesse usado '=' em vez de 'gostar', mas tenho uma premonição de que o último está prestes a me salvar de digitar.

Acontece que o responsável pelo zoológico acha que ele pode ter entrado em alguns dos cisnes como 'pretos', então eu modifico a consulta de acordo:

select top 10 * 
from animal  
where colour like 'black%' 
and species like 'swan'

Acontece que também não há nenhum deles (e, de fato, não há animais em '% preto', exceto os 'pretos'), mas a consulta agora leva cerca de 30 segundos para retornar vazia.

Parece que é apenas a combinação de 'top' e 'like%' causando problemas, porque

select count(*) 
from animal  
where colour like 'black%' 
and species like 'swan'

retorna 0 muito rapidamente e até

select * 
from animal 
where colour like 'black%' 
and species like 'swan'

retorna vazio em uma fração de segundo.

Alguém tem alguma idéia de por que 'top' e '%' devem conspirar para causar uma perda de desempenho tão dramática, especialmente em um conjunto de resultados vazio?

EDIT: Apenas para esclarecer, não estou usando nenhum índice do FreeText, apenas quis dizer que os campos são de texto livre no ponto de entrada, ou seja, não são normalizados no banco de dados. Desculpem a confusão, palavras pobres da minha parte.

stovroz
fonte

Respostas:

76

Esta é uma decisão do otimizador baseado em custos.

Os custos estimados utilizados nesta opção estão incorretos, pois pressupõe independência estatística entre valores em diferentes colunas.

É semelhante ao problema descrito em Objetivos de linha desonestos, em que os números pares e ímpares estão correlacionados negativamente.

É fácil de reproduzir.

CREATE TABLE dbo.animal(
    id int IDENTITY(1,1) NOT NULL PRIMARY KEY,
    colour varchar(50) NOT NULL,
    species varchar(50) NOT NULL,
    Filler char(10) NULL
);

/*Insert 20 million rows with 1% black and 1% swan but no black swans*/
WITH T
     AS (SELECT TOP 20000000 ROW_NUMBER() OVER (ORDER BY @@SPID) AS RN
         FROM   master..spt_values v1,
                master..spt_values v2,
                master..spt_values v3)
INSERT INTO dbo.animal
            (colour,
             species)
SELECT CASE
         WHEN RN % 100 = 1 THEN 'black'
         ELSE CAST(RN % 100 AS VARCHAR(3))
       END,
       CASE
         WHEN RN % 100 = 2 THEN 'swan'
         ELSE CAST(RN % 100 AS VARCHAR(3))
       END
FROM   T 

/*Create some indexes*/
CREATE NONCLUSTERED INDEX ix_species ON  dbo.animal(species);
CREATE NONCLUSTERED INDEX ix_colour ON  dbo.animal(colour);

Agora tente

SELECT TOP 10 *
FROM   animal
WHERE  colour LIKE 'black'
       AND species LIKE 'swan' 

Isso fornece o plano abaixo do qual é custado 0.0563167.

insira a descrição da imagem aqui

O plano pode executar uma junção de mesclagem entre os resultados dos dois índices na idcoluna. ( Mais detalhes do algoritmo de junção de mesclagem aqui ).

A junção de mesclagem requer que ambas as entradas sejam ordenadas pela chave de junção.

Os índices não clusterizados são ordenados por (species, id)e (colour, id)respectivamente (índices não exclusivos não agrupados em cluster sempre têm o localizador de linhas adicionado ao final da chave implicitamente, se não adicionado explicitamente). A consulta sem curingas está executando uma busca de igualdade em species = 'swan'e colour ='black'. Como cada busca está recuperando apenas um valor exato da coluna inicial, as linhas correspondentes serão ordenadas, idportanto, este plano é possível.

Os operadores do plano de consulta são executados da esquerda para a direita . Com o operador esquerdo solicitando linhas de seus filhos, que, por sua vez, solicitam linhas de seus filhos (e assim por diante até que os nós das folhas sejam atingidos). O TOPiterador irá parar de solicitar mais linhas de seu filho assim que 10 forem recebidos.

O SQL Server possui estatísticas sobre os índices que informam que 1% das linhas correspondem a cada predicado. Ele pressupõe que essas estatísticas são independentes (ou seja, não estão correlacionadas positiva ou negativamente), de modo que, em média, depois de processar 1.000 linhas correspondentes ao primeiro predicado, ele encontrará 10 correspondentes ao segundo e poderá sair. (o plano acima mostra realmente 987 em vez de 1.000, mas próximo o suficiente).

De fato, como os predicados estão correlacionados negativamente, o plano real mostra que todas as 200.000 linhas correspondentes precisam ser processadas a partir de cada índice, mas isso é mitigado em certa medida porque as zero linhas unidas também significam que realmente foram necessárias zero pesquisas.

Compare com

SELECT TOP 10 *
FROM   animal
WHERE  colour LIKE 'black%'
       AND species LIKE 'swan' 

O que fornece o plano abaixo, que é custado em 0.567943

insira a descrição da imagem aqui

A adição do curinga à direita agora causou uma verificação de índice. O custo do plano ainda é bastante baixo para uma varredura em uma tabela de 20 milhões de linhas.

Adicionar querytraceon 9130mostra mais algumas informações

SELECT TOP 10 *
FROM   animal
WHERE  colour LIKE 'black%'
       AND species LIKE 'swan'       
OPTION (QUERYTRACEON 9130)  

insira a descrição da imagem aqui

Pode-se observar que o SQL Server calcula que só será necessário verificar cerca de 100.000 linhas antes de encontrar 10 correspondentes ao predicado e TOPparar de solicitar linhas.

Novamente, isso faz sentido com a suposição de independência como 10 * 100 * 100 = 100,000

Finalmente, vamos tentar forçar um plano de interseção de índice

SELECT TOP 10 *
FROM   animal WITH (INDEX(ix_species), INDEX(ix_colour))
WHERE  colour LIKE 'black%'
       AND species LIKE 'swan' 

Isso fornece um plano paralelo para mim com custo estimado de 3,4625

insira a descrição da imagem aqui

A principal diferença aqui é que o colour like 'black%'predicado agora pode corresponder a várias cores diferentes. Isso significa que as linhas de índice correspondentes a esse predicado não são mais garantidas para serem classificadas na ordem de id.

Por exemplo, a busca de índice like 'black%'pode retornar as seguintes linhas

+------------+----+
|   Colour   | id |
+------------+----+
| black      | 12 |
| black      | 20 |
| black      | 23 |
| black      | 25 |
| blackberry |  1 |
| blackberry | 50 |
+------------+----+

Dentro de cada cor, os IDs são ordenados, mas os IDs de cores diferentes podem não ser.

Como resultado, o SQL Server não pode mais executar uma interseção de índice de junção de mesclagem (sem adicionar um operador de classificação de bloqueio) e opta por executar uma junção de hash. A junção de hash está bloqueando a entrada de construção; agora, o custo reflete o fato de que todas as linhas correspondentes precisarão ser processadas a partir da entrada de construção, em vez de assumir que ela precisará digitalizar 1.000, como no primeiro plano.

No entanto, a entrada da sonda não está bloqueando e ainda calcula incorretamente que será capaz de parar a análise depois de processar 987 linhas a partir dela.

(Mais informações sobre iteradores sem bloqueio versus bloqueio aqui)

Dado o aumento dos custos das linhas extras estimadas e a junção de hash, a verificação parcial do índice em cluster parece mais barata.

Na prática, é claro que a varredura de índice em cluster "parcial" não é de todo e precisa percorrer os 20 milhões de linhas inteiras, em vez dos 100 mil assumidos ao comparar os planos.

Aumentar o valor do TOP(ou removê-lo totalmente) eventualmente encontra um ponto de inflexão em que o número de linhas que ele estima que a varredura do IC precisará cobrir faz com que esse plano pareça mais caro e reverte para o plano de interseção do índice. Para mim, o ponto de corte entre os dois planos é TOP (89)vs TOP (90).

Para você, pode muito bem diferir, pois depende da largura do índice em cluster.

Removendo TOPe forçando a varredura de IC

SELECT *
FROM   animal WITH (INDEX = 1)
WHERE  colour LIKE 'black%'
       AND species LIKE 'swan' 

É custado 88.0586em minha máquina para minha tabela de exemplo.

Se o SQL Server estivesse ciente de que o zoológico não tinha cisnes negros e que precisaria fazer uma verificação completa em vez de apenas ler 100.000 linhas, esse plano não seria escolhido.

Eu tentei estatísticas múltiplas colunas sobre animal(species,colour)e animal(colour,species)e estatísticas filtrada em animal (colour) where species = 'swan'mas nenhum destes ajudar a convencer-se que não existem cisnes negros ea TOP 10necessidade fará a varredura de processar mais de 100.000 linhas.

Isso ocorre devido à "suposição de inclusão", na qual o SQL Server pressupõe que, se você estiver procurando por algo, ele provavelmente existe.

Em 2008+, há um sinalizador de rastreamento documentado 4138 que desativa as metas de linha. O efeito disso é que o plano é custado sem a suposição de que TOPisso permitirá que os operadores filhos terminem mais cedo sem ler todas as linhas correspondentes. Com esse sinalizador de rastreamento, recebo naturalmente o plano de interseção do índice mais ideal.

SELECT TOP 10 *
FROM   animal
WHERE  colour LIKE 'black%'
       AND species LIKE 'swan'
OPTION (QUERYTRACEON 4138)       

insira a descrição da imagem aqui

Agora, esse plano custa corretamente a leitura das 200 mil linhas completas nas duas pesquisas de índice, mas custa mais as pesquisas principais (estimadas em 2 mil x 0 reais. TOP 10Isso o restringiria a um máximo de 10, mas o sinalizador de rastreamento impede que isso seja levado em consideração) . Ainda assim, o plano tem um custo significativamente mais barato que a varredura completa do IC, por isso é selecionado.

É claro que este plano pode não ser o ideal para combinações que são comum. Como cisnes brancos.

Um índice composto animal (colour, species)ou idealmente animal (species, colour)permitiria que a consulta fosse muito mais eficiente para os dois cenários.

Para fazer o uso mais eficiente do índice composto LIKE 'swan', também seria necessário mudar para = 'swan'.

A tabela abaixo mostra os predicados de busca e predicados residuais mostrados nos planos de execução para todas as quatro permutações.

+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+
|                 WHERE clause                 |       Index       |                         Seek Predicate                         |              Residual Predicate              |
+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+
| colour LIKE 'black%' AND species LIKE 'swan' | ix_colour_species | colour >= 'black' AND colour < 'blacL'                         | colour like 'black%' AND species like 'swan' |
| colour LIKE 'black%' AND species LIKE 'swan' | ix_species_colour | species >= 'swan' AND species <= 'swan'                        | colour like 'black%' AND species like 'swan' |
| colour LIKE 'black%' AND species = 'swan'    | ix_colour_species | (colour,species) >= ('black', 'swan')) AND colour < 'blacL'    | colour LIKE 'black%' AND species = 'swan'    |
| colour LIKE 'black%' AND species = 'swan'    | ix_species_colour | species = 'swan' AND (colour >= 'black' and colour <  'blacL') | colour like 'black%'                         |
+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+
Martin Smith
fonte
15

Fundando isso intrigante, fiz algumas pesquisas e me deparei com este Q / A Como (e por que) o TOP afeta um plano de execução?

Basicamente, o uso do TOP altera o custo dos operadores que o seguem (de maneira não trivial), o que faz com que o plano geral mude também (seria ótimo se você incluísse o ExecPlans com e sem o TOP 10), o que praticamente altera a execução geral do A pergunta.

Espero que isto ajude.

Por exemplo, eu tentei em um banco de dados e: -quando o topo não é chamado, o paralelismo é usado -com TOP, o paralelismo não é usado

Então, novamente, mostrar seus planos de execução forneceria mais informações.

Tenha um bom dia

Vladislav Zalesak
fonte
-1

Acredito que isso possa ser devido à natureza subjacente do MSSQL 2005 e à maneira como o otimizador de consultas decide qual plano de execução é o mais eficiente.

Se você usar uma variável SQL, deve 'enganar' o otimizador de consulta para usar correspondências de hash em vez de loops aninhados, o que resultará em um grau muito maior de paralelismo.

Experimentar:

DECLARE @topn INT = 10
SELECT TOP (@topn) *
FROM    animal
WHERE   colour LIKE 'black%' 
AND species LIKE 'swan'

fonte
5
Ofuscar o TOPvalor em uma variável significa que ele assumirá TOP 100e não TOP 10. Isso pode ou não ajudar, dependendo do ponto de inflexão entre os dois planos.
Martin Smith