web-dev-qa-db-fra.com

Requête lente lorsque la clause «contient» et «=» ensemble dans la clause where

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?

8
Hooman Valibeigi

Votre plan d'exécution

En regardant le plan de requête, nous pouvons voir qu'un index est touché pour servir deux opérations de filtrage.

enter image description here

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.

enter image description here


Recréer

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:

enter image description here

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*"');

enter image description here

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:

enter image description here

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.

enter image description here

enter image description here

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

Résolution

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.

enter image description here

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:

enter image description here

où la fonction de texte intégral est appliquée directement

enter image description here

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.

Source


Supplémentaire

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*"')  

enter image description here

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.

Source

Avec le prédicat de recherche appliqué à cette bobine d'index désireux:

enter image description here

11
Randi Vertongen