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.
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.
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:
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*"');
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:
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.
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.
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:
onde a função de texto completo é aplicada diretamente
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*"')
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: