J'ai un zoo de 20 millions d'animaux que je surveille sur ma base de données SQL Server 2005. Environ 1% d'entre eux sont noirs et environ 1% d'entre eux sont des cygnes. Je voulais obtenir des détails sur tous les cygnes noirs et donc, 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, par inadvertance, ces champs sont en texte libre, mais ils sont tous deux indexés). Il s'avère que nous n'avons pas de tels animaux 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 "j'aime" mais j'ai une prémonition, ce dernier est sur le point de me faire économiser de la frappe.
Il s'avère que le gardien de zoo pense qu'il a peut-être entré certains des cygnes comme "noirâtres", alors je modifie 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%" sauf ceux "noirs") mais la requête prend maintenant environ 30 secondes pour revenir vide.
Il semble que ce ne soit que la combinaison de "top" et "like%" qui cause des problèmes 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'
renvoie vide en une fraction de seconde.
Quelqu'un a-t-il une idée de pourquoi "top" et "%" devraient conspirer pour causer une perte de performances aussi dramatique, en particulier dans un ensemble de résultats vide?
EDIT: Juste pour clarifier, je n'utilise aucun index FreeText, je voulais juste dire que les champs sont du texte libre au point d'entrée, c'est-à-dire non normalisés dans la base de données. Désolé pour la confusion, mauvaise formulation de ma part.
Il s'agit d'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 des différentes colonnes.
Il est similaire au problème décrit dans Row Goals Gone Rogue où les nombres pairs et impairs sont corrélés négativement.
Il 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 évalué à 0.0563167
.
Le plan peut effectuer une jointure de fusion entre les résultats des deux index sur la colonne id
. ( Plus de détails sur l'algorithme de jointure par fusion ici ).
La jointure par fusion nécessite que les deux entrées soient ordonnées par la clé de jonction.
Les index non cluster sont classés respectivement par (species, id)
Et (colour, id)
(Index non uniques non groupés implicitement, le localisateur de ligne est toujours ajouté à la fin de la clé s'il n'est pas ajouté explicitement ). La requête sans caractères génériques effectue une recherche d'égalité dans species = 'swan'
Et colour ='black'
. Comme chaque recherche ne récupère qu'une seule valeur exacte de la colonne de tête, les lignes correspondantes seront ordonnées par id
donc ce plan est possible.
Opérateurs de plan de requête exécuter de gauche à droite . Avec l'opérateur gauche demandant des lignes à ses enfants, qui à leur tour demandent des lignes à leurs enfants (et ainsi de suite jusqu'à ce que les nœuds terminaux soient atteints). L'itérateur TOP
cessera de demander d'autres lignes à son enfant une fois que 10 auront été reçues.
SQL Server dispose de statistiques sur les index qui indiquent que 1% des lignes correspondent à chaque prédicat. Il suppose que ces statistiques sont indépendantes (c'est-à-dire qu'elles ne sont pas corrélées positivement ou négativement) de sorte qu'en moyenne une fois qu'il a traité 1000 lignes correspondant au premier prédicat, il en trouve 10 correspondant au second et peut sortir. (le plan ci-dessus montre en fait 987 plutôt que 1000 mais assez proche).
En fait, comme les prédicats sont corrélés négativement, le plan réel montre que les 200 000 lignes correspondantes devaient être traitées à partir de chaque index, mais cela est atténué dans une certaine mesure car les lignes jointes à zéro signifient également que zéro recherche était 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 évalué à 0.567943
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, mais pour une analyse sur une table de 20 millions de lignes.
L'ajout de querytraceon 9130
Affiche plus d'informations
SELECT TOP 10 *
FROM animal
WHERE colour LIKE 'black%'
AND species LIKE 'swan'
OPTION (QUERYTRACEON 9130)
On peut voir que SQL Server estime qu'il lui suffit d'analyser environ 100 000 lignes avant de trouver 10 correspondant au prédicat et que TOP
peut arrêter de demander des lignes.
Encore une fois, cela a du sens avec l'hypothèse d'indépendance comme 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é à 3,4625
La principale différence ici est que le prédicat colour like 'black%'
Peut désormais 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 dans l'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 peuvent ne pas l'être.
Par conséquent, SQL Server ne peut plus effectuer une intersection d'index de jointure de fusion (sans ajouter d'opérateur de tri bloquant) et il opte pour effectuer une jointure de hachage à la place. Hash Join bloque l'entrée de génération, donc le coût reflète maintenant le fait que toutes les lignes correspondantes devront être traitées à partir de l'entrée de génération plutôt que de supposer qu'il n'aura qu'à scanner 1000 comme dans le premier plan.
L'entrée de sonde n'est cependant pas bloquante et elle estime toujours à tort qu'elle pourra arrêter la vérification après avoir traité 987 lignes à partir de cela.
(Plus d'informations sur les itérateurs non bloquants et bloquants ici)
Compte tenu de l'augmentation des coûts des lignes supplémentaires estimées et du hachage, l'analyse d'index en cluster partiel semble moins coûteuse.
Dans la pratique, bien sûr, le balayage d'index clusterisé "partiel" n'est pas du tout partiel et il doit parcourir les 20 millions de lignes au lieu des 100 000 supposées lors de la comparaison des plans.
L'augmentation de la valeur de TOP
(ou sa suppression totale) rencontre finalement un point de basculement où le nombre de lignes qu'il estime que l'analyse CI devra couvrir rend ce plan plus cher et revient au plan d'intersection d'index . Pour moi, le point de coupure entre les deux plans est TOP (89)
vs TOP (90)
.
Pour vous, cela peut bien différer car cela dépend de la largeur de l'index cluster.
Suppression de TOP
et forçage de l'analyse CI
SELECT *
FROM animal WITH (INDEX = 1)
WHERE colour LIKE 'black%'
AND species LIKE 'swan'
Est coûté à 88.0586
Sur 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 de faire une analyse complète plutôt que de simplement lire 100 000 lignes, ce plan ne serait pas choisi.
J'ai essayé les statistiques multi-colonnes sur animal(species,colour)
et animal(colour,species)
et filtré les statistiques sur animal (colour) where species = 'swan'
mais rien de tout cela ne l'a convaincu que les cygnes noirs n'existent pas et que L'analyse TOP 10
Devra traiter plus de 100 000 lignes.
Cela est dû à "l'hypothèse d'inclusion" où SQL Server suppose essentiellement que si vous recherchez quelque chose, il existe probablement.
Sur 2008+, il y a un indicateur de trace documenté 4138 qui désactive les objectifs de ligne. L'effet de ceci est que le plan est chiffré sans l'hypothèse que le TOP
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 le plus optimal.
SELECT TOP 10 *
FROM animal
WHERE colour LIKE 'black%'
AND species LIKE 'swan'
OPTION (QUERYTRACEON 4138)
Ce plan coûte désormais correctement la lecture des 200 000 lignes complètes dans les deux recherches d'index, mais coûte plus cher que les recherches clés (estimé à 2 000 par rapport à 0 réel. Le TOP 10
Limiterait cela à un maximum de 10 mais l'indicateur de trace empêche ceci étant pris en compte). Pourtant, le plan coûte beaucoup moins cher que le balayage CI complet, il est donc sélectionné.
Bien sûr, ce plan peut ne pas être optimal pour les combinaisons qui sont courantes. Tels que des cygnes blancs.
Un index composite sur animal (colour, species)
ou idéalement animal (species, colour)
permettrait à la requête d'être beaucoup plus efficace pour les deux scénarios.
Pour utiliser plus efficacement l'index composite, le LIKE 'swan'
Devrait également être changé en = '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%' |
+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+
Fondant cette intrigante, j'ai fait quelques recherches et suis tombé sur cette Q/R Comment (et pourquoi) TOP a-t-il un impact sur un plan d'exécution?
Fondamentalement, l'utilisation de TOP modifie le coût des opérateurs qui la suivent (de manière non triviale), ce qui entraîne également une modification du plan général (ce serait formidable si vous incluiez ExecPlans avec et sans TOP 10), ce qui modifie à peu près 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 sommet n'est invoqué, 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