La requête suivante prend environ 10 secondes pour terminer sur une table avec 12k enregistrements
select top (5) *
from "Physician"
where "id" = 1 or contains("lastName", '"a*"')
Mais si je change la clause where en
where "id" = 1
ou
where contains("lastName", '"a*"')
Il reviendra instantanément.
Les deux colonnes sont indexées et la colonne lastName est également indexée en texte intégral.
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;
Voici le Plan d'exécution
Quel pourrait être le problème?
En regardant le plan de requête, nous pouvons voir qu'un index est touché pour servir deux opérations de filtrage.
En termes très simples, en raison de l'opérateur TOP, un objectif de ligne a été fixé. Vous trouverez beaucoup plus d'informations et de conditions préalables sur les objectifs des rangées ici
De cette même source:
Une stratégie d'objectif de ligne signifie généralement privilégier les opérations de navigation non bloquantes (par exemple, les jointures de boucles imbriquées, les recherches d'index et les recherches) par rapport aux opérations de blocage basées sur des ensembles comme le tri et le hachage. Cela peut être utile chaque fois que le client peut bénéficier d'un démarrage rapide et d'un flux régulier de lignes (avec peut-être un temps d'exécution global plus long - voir le post de Rob Farley ci-dessus). Il y a aussi les utilisations les plus évidentes et traditionnelles, par exemple en présentant les résultats une page à la fois.
La table entière est sondée dans les filtres à l'aide d'une semi-jointure gauche qui a un objectif de ligne défini, dans l'espoir de renvoyer les 5 lignes aussi rapidement et efficacement que possible.
Cela ne se produit pas, ce qui entraîne de nombreuses itérations sur le .Fulltextmatch TVF.
Basé sur votre plan , j'ai pu recréer quelque peu votre problème:
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);
Exécution de la requête
SELECT TOP (5) *
FROM dbo.Person
WHERE "id" = 1 OR contains("lastName", '"B*"');
Résultats dans un plan de requête comparable au vôtre:
Dans l'exemple ci-dessus, B n'existe pas dans l'index de texte intégral. En conséquence, cela dépend du paramètre et des données de l'efficacité du plan de requête.
Une meilleure explication de ceci peut être trouvée dans Row Goals, Part 2: Semi Joins par Paul White
... En d'autres termes, à chaque itération d'une application, nous pouvons arrêter de regarder l'entrée B dès que la première correspondance est trouvée, en utilisant le prédicat de jointure poussé. C'est exactement le genre de chose pour laquelle un objectif de ligne est bon: générer une partie d'un plan optimisé pour renvoyer rapidement les n premières lignes correspondantes (où n = 1 ici).
Par exemple, changer le prédicat pour que les résultats soient trouvés bien plus tôt (au début de l'analyse).
select top (5) *
from dbo.Person
where "id" = 124
or contains("lastName", '"A*"');
where "id" = 124
est éliminé car le prédicat d'index de texte intégral renvoie déjà 5 lignes, satisfaisant le prédicat TOP()
.
Les résultats le montrent également
id lastname
1 'AAA...'
2 'AAA...'
3 'AAA...'
4 'AAA...'
5 'AAA...'
Et les exécutions de TVF:
Insertion de nouvelles lignes
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);
Exécution de la requête pour rechercher ces lignes insérées précédentes
SELECT TOP (2) *
from dbo.Person
where "id" = 1
or contains("lastName", '"B*"');
Cela entraîne à nouveau trop d'itérations sur presque toutes les lignes pour renvoyer la dernière valeur trouvée.
id lastname
1 'AAA...'
12001 'BBB...'
Lors de la suppression de l'objectif de ligne à l'aide de traceflag 4138
SELECT TOP (5) *
FROM dbo.Person
WHERE "id" = 124
OR contains("lastName", '"B*"')
OPTION(QUERYTRACEON 4138 );
L'optimiseur utilise un modèle de jointure plus proche de l'implémentation d'un UNION
, dans notre cas, cela est favorable car il pousse les prédicats vers le bas vers leurs recherches d'index cluster respectives, et n'utilise pas l'opérateur de demi-jointure gauche ciblé par ligne.
Une autre façon d'écrire ceci, sans utiliser le traceflag mentionné ci-dessus:
SELECT top (5) *
FROM
(
SELECT *
FROM dbo.Person
WHERE "id" = 1
UNION
SELECT *
FROM dbo.Person
WHERE contains("lastName", '"B*"')
) as A;
Avec le plan de requête résultant:
où la fonction de texte intégral est appliquée directement
En guise de note, pour op, le correctif de l'optimiseur de requêtes traceflag 4199 a résolu son problème. Il a implémenté cela en ajoutant OPTION(QUERYTRACEON(4199))
à la requête. Je n'ai pas pu reproduire ce comportement de mon côté. Ce correctif contient une optimisation de semi-jointure:
Indicateur de trace: 4102 Fonction: SQL 9 - Les performances de la requête sont lentes si le plan d'exécution de la requête contient des opérateurs de semi-jointure En règle générale, les opérateurs de semi-jointure sont générés lorsque la requête contient le mot clé IN ou le mot clé EXISTS. Activez les indicateurs 4102 et 4118 pour résoudre ce problème.
Pendant l'optimisation basée sur les coûts, l'optimiseur peut également ajouter une bobine d'index au plan d'exécution, implémentée par LogOp_Spool Index on fly Eager
(ou l'équivalent physique)
Il le fait avec mon jeu de données pour TOP(3)
mais pas pour TOP(2)
SELECT TOP (3) *
from dbo.Physician
where "id" = 1
or contains("lastName", '"B*"')
Lors de la première exécution, un spool impatient lit et stocke l'intégralité de l'entrée avant de renvoyer le sous-ensemble de lignes qui est demandé par les exécutions Predicate Later lire et renvoyer le même ou un sous-ensemble différent de lignes de la table de travail, sans jamais avoir à exécuter l'enfant nœuds à nouveau.
Avec le prédicat de recherche appliqué à cette bobine d'index désireux: