Ralentissement considérable de la requête SQL Server lors de l'ajout d'un caractère générique (ou top)

52

J'ai un zoo de 20 millions d'animaux que je trace sur ma base de données SQL Server 2005. Environ 1% d'entre eux sont noirs et environ 1% sont des cygnes. Je voulais avoir des détails sur tous les cygnes noirs et ainsi, ne voulant pas submerger la page de résultats, je l'ai fait:

select top 10 * 
from animal 
where colour like 'black'  
and species like 'swan'

(Oui, à tort, ces champs sont en texte libre, mais ils sont tous deux indexés). Il s’avère que nous n’avons aucun animal de ce type, car la requête renvoie un ensemble vide dans environ 300 millisecondes. Cela aurait été environ deux fois plus rapide si j'avais utilisé '=' plutôt que 'comme' mais j'ai une prémonition que ce dernier est sur le point de m'épargner de la dactylographie.

Il s'avère que le gardien de zoo pense qu'il a peut-être saisi certains cygnes comme étant «noirâtres». Je modifie donc la requête en conséquence:

select top 10 * 
from animal  
where colour like 'black%' 
and species like 'swan'

Il s'avère qu'il n'y en a pas non plus (et en fait, il n'y a pas d'animaux '% noirs' à l'exception des animaux 'noirs'), mais la requête prend maintenant environ 30 secondes pour retourner vide.

Il semble que ce n’est que la combinaison de "top" et de "like%" qui pose problème, car

select count(*) 
from animal  
where colour like 'black%' 
and species like 'swan'

renvoie 0 très rapidement, et même

select * 
from animal 
where colour like 'black%' 
and species like 'swan'

retourne vide en une fraction de seconde.

Quelqu'un a-t-il une idée de la raison pour laquelle "top" et "%" devraient s'entendre pour causer une perte de performance aussi spectaculaire, en particulier dans un ensemble de résultats vide?

EDIT: Juste pour clarifier les choses, je n’utilise aucun index FreeText, je voulais simplement dire que les champs sont en mode libre au point d’entrée, c’est-à-dire qu’ils ne sont pas normalisés dans la base de données. Désolé pour la confusion, mauvaise formulation de ma part.

Stovroz
la source

Réponses:

76

C'est une décision de l'optimiseur basé sur les coûts.

Les coûts estimés utilisés dans ce choix sont incorrects car ils supposent une indépendance statistique entre les valeurs de différentes colonnes.

Ce problème est similaire au problème décrit dans Rangée d'objectifs en rangs, où les nombres pairs et impairs sont corrélés négativement.

C'est facile à reproduire.

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);

Maintenant essaye

SELECT TOP 10 *
FROM   animal
WHERE  colour LIKE 'black'
       AND species LIKE 'swan' 

Cela donne le plan ci-dessous qui est chiffré à 0.0563167.

entrez la description de l'image ici

Le plan peut effectuer une jointure de fusion entre les résultats des deux index de la idcolonne. ( Plus de détails sur l'algorithme de jointure de fusion ici ).

La jointure par fusion nécessite que les deux entrées soient ordonnées par la clé de jonction.

Les index non clusterisés sont classés par (species, id)et (colour, id)respectivement (les index de ligne non indexés non uniques ont toujours le localisateur de ligne ajouté à la fin de la clé de manière implicite si ce n'est pas ajouté explicitement). La requête sans caractère générique effectue une recherche d’égalité dans species = 'swan'et colour ='black'. Comme chaque recherche ne récupère qu'une valeur exacte de la colonne de tête, les lignes correspondantes seront classées, idce qui rend ce plan possible.

Les opérateurs de plan de requête s'exécutent de gauche à droite . Avec l'opérateur de gauche demandant des lignes à ses enfants, qui demandent à leur tour des lignes à leurs enfants (et ainsi de suite jusqu'à ce que les noeuds feuille soient atteints). L' TOPitérateur arrêtera de demander plus de lignes à son enfant une fois que 10 auront été reçues.

SQL Server dispose de statistiques sur les index lui indiquant que 1% des lignes correspondent à chaque prédicat. Il suppose que ces statistiques sont indépendantes (c'est-à-dire qu'elles ne sont corrélées ni positivement ni négativement), de sorte qu'en moyenne, une fois qu'il a traité 1 000 lignes correspondant au premier prédicat, il en trouve 10 correspondant au second et peut sortir. (Le plan ci-dessus montre en réalité 987 plutôt que 1 000, mais suffisamment proches).

En fait, comme les prédicats sont corrélés négativement, le plan actuel montre que toutes les 200 000 lignes correspondantes doivent être traitées à partir de chaque index, mais ceci est atténué dans une certaine mesure car les lignes jointes à zéro signifient également qu'aucune recherche n'est réellement nécessaire.

Comparer avec

SELECT TOP 10 *
FROM   animal
WHERE  colour LIKE 'black%'
       AND species LIKE 'swan' 

Ce qui donne le plan ci-dessous qui est chiffré à 0.567943

entrez la description de l'image ici

L'ajout du caractère générique de fin a maintenant provoqué une analyse d'index. Le coût du plan est encore assez faible pour une analyse sur une table de 20 millions de lignes.

L'ajout querytraceon 9130montre plus d'informations

SELECT TOP 10 *
FROM   animal
WHERE  colour LIKE 'black%'
       AND species LIKE 'swan'       
OPTION (QUERYTRACEON 9130)  

entrez la description de l'image ici

On peut constater que SQL Server estime qu'il lui suffira d'analyser environ 100 000 lignes avant de trouver 10 correspondances avec le prédicat et de TOPne plus pouvoir demander de lignes.

Encore une fois, cela a du sens avec l'hypothèse d'indépendance 10 * 100 * 100 = 100,000

Enfin, essayons de forcer un plan d’intersection d’index

SELECT TOP 10 *
FROM   animal WITH (INDEX(ix_species), INDEX(ix_colour))
WHERE  colour LIKE 'black%'
       AND species LIKE 'swan' 

Cela donne un plan parallèle pour moi avec un coût estimé de 3,4625.

entrez la description de l'image ici

La principale différence ici est que le colour like 'black%'prédicat peut maintenant correspondre à plusieurs couleurs différentes. Cela signifie que les lignes d'index correspondantes pour ce prédicat ne sont plus garanties d'être triées par ordre de id.

Par exemple, la recherche d'index sur like 'black%'peut renvoyer les lignes suivantes

+------------+----+
|   Colour   | id |
+------------+----+
| black      | 12 |
| black      | 20 |
| black      | 23 |
| black      | 25 |
| blackberry |  1 |
| blackberry | 50 |
+------------+----+

Dans chaque couleur, les identifiants sont ordonnés, mais les identifiants de différentes couleurs risquent de ne pas l'être.

Par conséquent, SQL Server ne peut plus effectuer d'intersection d'index de jointure de fusion (sans ajouter d'opérateur de tri bloquant) et opte plutôt pour une jointure de hachage. La jointure de hachage bloque sur l'entrée de génération. Le coût reflète donc maintenant le fait que toutes les lignes correspondantes doivent être traitées à partir de l'entrée de construction plutôt que de supposer qu'il ne faudra numériser que 1 000 comme dans le premier plan.

Cependant, l’entrée de sondage n’est pas bloquante et elle estime toujours à tort qu’elle sera en mesure de cesser de sonder après le traitement de 987 lignes.

(Plus d'informations sur les itérateurs non bloquants ou bloquants ici)

Compte tenu de l'augmentation des coûts des lignes supplémentaires estimées et de la jonction de hachage, l'analyse partielle par cluster semble moins chère.

En pratique, bien entendu, l’analyse "partielle" des index clusterisés n’est pas du tout partielle et doit parcourir 20 millions de lignes au lieu des 100 000 supposées lors de la comparaison des plans.

L'augmentation de la valeur de l' TOPélément (ou sa suppression complète) finit par rencontrer un point de basculement où le nombre de lignes qu'il estime nécessaire de couvrir de l'analyse de CI rend ce plan plus cher et revient au plan d'intersection d'index. Pour moi , la coupure point entre les deux plans est TOP (89)contre TOP (90).

Pour vous, cela peut différer car cela dépend de la largeur de l'index clusterisé.

Retirer TOPet forcer le scan de CI

SELECT *
FROM   animal WITH (INDEX = 1)
WHERE  colour LIKE 'black%'
       AND species LIKE 'swan' 

Est chiffré à 88.0586sur ma machine pour mon exemple de table.

Si SQL Server savait que le zoo n'avait pas de cygnes noirs et qu'il aurait besoin d'effectuer une analyse complète au lieu de simplement lire 100 000 lignes, ce plan ne serait pas choisi.

J'ai essayé les statistiques de plusieurs colonnes sur animal(species,colour)et animal(colour,species)et les statistiques Filtré sur , animal (colour) where species = 'swan'mais aucune de ces aide convainquent que les cygnes noirs existent pas et le TOP 10besoin d' analyse de volonté de traiter plus de 100.000 lignes.

Cela est dû à "l'hypothèse d'inclusion" dans laquelle SQL Server suppose essentiellement que si vous recherchez quelque chose, il existe probablement.

En 2008+, un indicateur de trace documenté 4138 désactive les objectifs de rangée. Cela a pour effet de chiffrer le plan sans supposer que TOPcela permettra aux opérateurs enfants de se terminer plus tôt sans lire toutes les lignes correspondantes. Avec cet indicateur de trace en place, j'obtiens naturellement le plan d'intersection d'index plus optimal.

SELECT TOP 10 *
FROM   animal
WHERE  colour LIKE 'black%'
       AND species LIKE 'swan'
OPTION (QUERYTRACEON 4138)       

entrez la description de l'image ici

Ce plan coûte désormais correctement pour la lecture des 200 000 lignes complètes dans les deux recherches d’index, mais sur les recherches de clé (estimé à 2 000 vs 0 réel. TOP 10Cela limiterait ce nombre à 10, mais l’indicateur de trace empêche que cela soit pris en compte). . Néanmoins, le coût du plan est nettement inférieur à celui de l'analyse complète des éléments de configuration, de sorte qu'il a été sélectionné.

Bien sûr , ce plan pourrait ne pas être optimale pour les combinaisons qui sont communes. Comme les cygnes blancs.

Un index composite sur animal (colour, species)ou dans l'idéal animal (species, colour)permettrait à la requête d'être beaucoup plus efficace pour les deux scénarios.

Pour utiliser au mieux l’indice composite, il LIKE 'swan'faudrait également le remplacer par = 'swan'.

Le tableau ci-dessous montre les prédicats de recherche et les prédicats résiduels indiqués dans les plans d'exécution pour les quatre permutations.

+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+
|                 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%'                         |
+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+
Martin Smith
la source
15

Fondant cet intrigant, j'ai fait quelques recherches et suis tombé sur cette question. Comment (et pourquoi) TOP at-il un impact sur un plan d'exécution?

Fondamentalement, l'utilisation de TOP modifie le coût des opérateurs qui le suivent (de manière non triviale), ce qui entraîne également une modification du plan global (il serait bien de pouvoir inclure ExecPlans avec et sans TOP 10), ce qui modifie sensiblement l'exécution globale de la requête.

J'espère que cela t'aides.

Par exemple, je l'ai essayé sur une base de données et: -Quand aucun top n'est appelé, le parallélisme est utilisé -avec TOP, le parallélisme n'est pas utilisé

Donc, encore une fois, montrer vos plans d'exécution fournirait plus d'informations.

Bonne journée

Vladislav Zalesak
la source
-1

Je pense que cela peut être dû à la nature sous-jacente de MSSQL 2005 et à la façon dont l'optimiseur de requêtes décide quel plan d'exécution est le plus efficace.

Si vous utilisez une variable SQL, l'optimiseur de requête doit «tromper» en utilisant des correspondances de hachage au lieu de boucles imbriquées, ce qui entraînera un degré de parallélisme beaucoup plus élevé.

Essayer:

DECLARE @topn INT = 10
SELECT TOP (@topn) *
FROM    animal
WHERE   colour LIKE 'black%' 
AND species LIKE 'swan'

la source
5
Le fait de masquer la TOPvaleur d’une variable signifie qu’elle assumera TOP 100plutôt que TOP 10. Cela peut aider ou non, selon le point de basculement entre les deux plans.
Martin Smith