Consulta lenta ao ter 'contém' e '=' juntos na cláusula where

8

A consulta a seguir leva cerca de 10 segundos para terminar em uma tabela com 12k registros

select top (5) *
from "Physician"
where "id" = 1 or contains("lastName", '"a*"')

Mas se eu mudar a cláusula where para

where "id" = 1

ou

where contains("lastName", '"a*"')

Ele retornará instantaneamente.

Ambas as colunas são indexadas e a coluna lastName também é indexada em texto completo.

CREATE TABLE Physician
(
   id         int identity    NOT NULL,
   firstName  nvarchar(100)   NOT NULL,
   lastName   nvarchar(100)   NOT NULL
);

ALTER TABLE Physician
  ADD CONSTRAINT Physician_PK
  PRIMARY KEY CLUSTERED (id);

CREATE NONCLUSTERED INDEX Physician_IX2
   ON Physician (firstName ASC);

CREATE NONCLUSTERED INDEX Physician_IX3
   ON Physician (lastName ASC);

CREATE FULLTEXT INDEX
    ON "Physician" ("firstName" LANGUAGE 0x0, "lastName" LANGUAGE 0x0)
    KEY INDEX "Physician_PK"
    ON "the_catalog"
    WITH stoplist = off;

Aqui está o plano de execução

Qual poderia ser o problema?

Hooman Valibeigi
fonte
Acabei de adicionar a definição da tabela
Hooman Valibeigi 16/09/19

Respostas:

11

Seu plano de execução

Ao analisar o plano de consulta, podemos ver que um índice é tocado para atender a duas operações de filtro.

insira a descrição da imagem aqui

Em termos simples, devido ao operador TOP, uma meta de linha foi definida. Muito mais informações e pré-requisitos sobre metas de linha podem ser encontrados aqui

Da mesma fonte:

Uma estratégia de meta de linha geralmente significa favorecer operações de navegação sem bloqueio (por exemplo, junções de loops aninhados, pesquisas de índice e pesquisas) sobre operações de bloqueio baseadas em conjuntos, como classificação e hash. Isso pode ser útil sempre que o cliente puder se beneficiar de uma inicialização rápida e um fluxo constante de linhas (talvez com um tempo de execução geral mais longo - veja a postagem de Rob Farley acima). Existem também os usos mais óbvios e tradicionais, por exemplo, na apresentação de resultados de uma página por vez.

A tabela inteira é sondada nos filtros com o uso de uma semi junção esquerda que tem uma meta de linha definida, na esperança de retornar as 5 linhas o mais rápido e eficiente possível.

Isso não acontece, resultando em muitas iterações sobre o TVF .Fulltextmatch.

insira a descrição da imagem aqui


Recriando

Com base no seu plano , consegui recriar um pouco o seu problema:

CREATE TABLE dbo.Person(id int not null,lastname varchar(max));

CREATE UNIQUE INDEX ui_id ON  dbo.Person(id)
CREATE FULLTEXT CATALOG ft AS DEFAULT;  
CREATE FULLTEXT INDEX ON dbo.Person(lastname)   
   KEY INDEX ui_id   
   WITH STOPLIST = SYSTEM;  
GO  

INSERT INTO dbo.Person(id,lastname)
SELECT top(12000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)),
REPLICATE(CAST('A' as nvarchar(max)),80000)+ CAST(ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) as varchar(10))
FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2;
CREATE CLUSTERED INDEX cx_Id on dbo.Person(id);

Executando a consulta

SELECT TOP (5) *
FROM dbo.Person
WHERE "id" = 1 OR contains("lastName", '"B*"');

Resultados em um plano de consulta comparável ao seu:

insira a descrição da imagem aqui

No exemplo acima, B não existe no índice de texto completo. Como resultado, depende do parâmetro e dos dados a eficiência do plano de consulta.

Uma explicação melhor disso pode ser encontrada em Row Goals, Part 2: Semi Joins por Paul White

... Em outras palavras, em cada iteração de uma aplicação, podemos parar de olhar para a entrada B assim que a primeira correspondência for encontrada, usando o predicado de junção push-down. É exatamente para isso que uma meta de linha é boa: gerar parte de um plano otimizado para retornar as primeiras n linhas correspondentes rapidamente (onde n = 1 aqui).

Por exemplo, alterando o predicado para que os resultados sejam encontrados mais cedo (no início da varredura).

select top (5) *
from dbo.Person
where "id" = 124 
or contains("lastName", '"A*"');

insira a descrição da imagem aqui

o where "id" = 124é eliminado devido ao predicado do índice de texto completo já retornar 5 linhas, satisfazendo o TOP()predicado.

Os resultados mostram isso também

id lastname 
1  'AAA...'   
2  'AAA...'
3  'AAA...'
4  'AAA...'
5  'AAA...'

E as execuções de TVF:

insira a descrição da imagem aqui

Inserindo algumas novas linhas

INSERT INTO dbo.Person
SELECT 12001, REPLICATE(CAST('B' as nvarchar(max)),80000);
INSERT INTO dbo.Person
SELECT 12002, REPLICATE(CAST('B' as nvarchar(max)),80000);

Executando a consulta para encontrar essas linhas inseridas anteriores

SELECT TOP (2) *
from dbo.Person
where "id" = 1
or contains("lastName", '"B*"');

Isso novamente resulta em muitas iterações em quase todas as linhas para retornar o último, mas um valor encontrado.

insira a descrição da imagem aqui

insira a descrição da imagem aqui

id   lastname
1     'AAA...'
12001 'BBB...'

Resolver

Ao remover o objetivo da linha usando o traceflag 4138

SELECT TOP (5) *
FROM dbo.Person
WHERE "id" = 124 
OR contains("lastName", '"B*"')
OPTION(QUERYTRACEON 4138 );

O otimizador usa um padrão de junção mais próximo da implementação de a UNION; no nosso caso, isso é favorável, pois empurra os predicados para as respectivas buscas de índice em cluster e não usa o operador de semi junção esquerda com objetivo de linha.

insira a descrição da imagem aqui

Outra maneira de escrever isso, sem usar o traceflag acima mencionado:

SELECT top (5) *
FROM
(
SELECT * 
FROM dbo.Person
WHERE "id" = 1 
UNION
SELECT * 
FROM dbo.Person
WHERE contains("lastName", '"B*"')
 ) as A;

Com o plano de consulta resultante:

insira a descrição da imagem aqui

onde a função de texto completo é aplicada diretamente

insira a descrição da imagem aqui

Como nota de rodapé, por op, o hotfix do otimizador de consultas traceflag 4199 resolveu seu problema. Ele implementou isso adicionando OPTION(QUERYTRACEON(4199))à consulta. Não fui capaz de reproduzir esse comportamento do meu lado. Esse hotfix contém uma otimização de semi-junção:

Sinalizador de rastreamento: 4102 Função: SQL 9 - O desempenho da consulta é lento se o plano de execução da consulta contiver operadores de semi-junção Normalmente, operadores de semi-junção são gerados quando a consulta contém a palavra-chave IN ou a palavra-chave EXISTS. Ative os sinalizadores 4102 e 4118 para superar isso.

Fonte


Extra

Durante a otimização baseada em custos, o otimizador também pode adicionar um spool de índice ao plano de execução, implementado por LogOp_Spool Index on fly Eager (ou a contraparte física)

Faz isso com meu conjunto de dados para, TOP(3)mas não paraTOP(2)

SELECT TOP (3) *
from dbo.Physician
where "id" = 1
or contains("lastName", '"B*"')  

insira a descrição da imagem aqui

Na primeira execução, um spool ansioso lê e armazena toda a entrada antes de retornar o subconjunto de linhas solicitado pelas execuções do Predicate Later e ler e retornar o mesmo ou um subconjunto de linhas da tabela de trabalho, sem precisar executar o filho nós novamente.

Fonte

Com o predicado de busca aplicado a este spool ansioso do índice:

insira a descrição da imagem aqui

Randi Vertongen
fonte
Você poderia explicar o uso das bandeiras de rastreamento? É um pouco incerto do seu código que eles estão fazendo
George.Palacios
11
@ George.Palacios Sim, eu fiz uma bagunça de tudo: ^). Fará obrigado pelo feedback!
Randi Vertongen
Não um dos sinalizadores QUERYTRACEON sugeridos (4138, 3604, 8607, 8612) funcionou, mas o QUERYTRACEON 4199 resolveu o problema !!!!
Hooman Valibeigi 16/09/19
Observe que a consulta é lenta, mesmo sem o operador TOP
Hooman Valibeigi
@HoomanValibeigi você tentou a solução da união na parte inferior?
Randi Vertongen