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
.
O plano pode executar uma junção de mesclagem entre os resultados dos dois índices na id
coluna. ( 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, id
portanto, 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 TOP
iterador 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
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 9130
mostra mais algumas informações
SELECT TOP 10 *
FROM animal
WHERE colour LIKE 'black%'
AND species LIKE 'swan'
OPTION (QUERYTRACEON 9130)
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 TOP
parar 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
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 TOP
e forçando a varredura de IC
SELECT *
FROM animal WITH (INDEX = 1)
WHERE colour LIKE 'black%'
AND species LIKE 'swan'
É custado 88.0586
em 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 10
necessidade 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 TOP
isso 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)
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 10
Isso 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%' |
+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+
TOP
valor em uma variável significa que ele assumiráTOP 100
e nãoTOP 10
. Isso pode ou não ajudar, dependendo do ponto de inflexão entre os dois planos.