Sommaire
Les principaux problèmes sont:
- La sélection de plan de l'optimiseur suppose une distribution uniforme des valeurs.
- Un manque d'indices appropriés signifie:
- La numérisation de la table est la seule option.
- La jointure est une jointure de boucles imbriquées naïve , plutôt qu'une jointure de boucles imbriquées d' index . Dans une jointure naïve, les prédicats de jointure sont évalués au niveau de la jointure plutôt que d'être poussés vers le bas du côté intérieur de la jointure.
Détails
Les deux plans sont fondamentalement assez similaires, bien que les performances puissent être très différentes:
Plan avec les colonnes supplémentaires
Prendre celui avec les colonnes supplémentaires qui ne se termine pas dans un délai raisonnable:
Les fonctionnalités intéressantes sont:
- Le sommet au nœud 0 limite les lignes renvoyées à 100. Il définit également un objectif de ligne pour l'optimiseur, de sorte que tout ce qui se trouve en dessous dans le plan est choisi pour renvoyer rapidement les 100 premières lignes.
- L'analyse au nœud 4 trouve des lignes de la table où le
Start_Time
n'est pas nul, State
est 3 ou 4 et Operation_Type
est l'une des valeurs répertoriées. Le tableau est entièrement analysé une fois, chaque ligne étant testée par rapport aux prédicats mentionnés. Seules les lignes qui réussissent tous les tests sont transmises au tri. L'optimiseur estime que 38 283 lignes seront admissibles.
- Le tri au nœud 3 consomme toutes les lignes du scan au nœud 4 et les trie par ordre de
Start_Time DESC
. Il s'agit de l'ordre de présentation final demandé par la requête.
- L'optimiseur estime que 93 lignes (en fait 93.2791) devront être lues à partir du tri pour que le plan entier retourne 100 lignes (en tenant compte de l'effet attendu de la jointure).
- La jointure des boucles imbriquées au nœud 2 devrait exécuter son entrée interne (la branche inférieure) 94 fois (en fait 94,2791). La ligne supplémentaire est requise par l'échange d'arrêt de parallélisme au nœud 1 pour des raisons techniques.
- L'analyse au nœud 5 analyse entièrement la table à chaque itération. Il trouve des lignes où
Start_Time
n'est pas nul et State
vaut 3 ou 4. On estime que cela produira 400 875 lignes à chaque itération. Sur 94,2791 itérations, le nombre total de lignes est de près de 38 millions.
- La jointure des boucles imbriquées au nœud 2 applique également les prédicats de jointure. Il vérifie ce qui
Operation_Type
correspond, que le Start_Time
noeud 4 est inférieur au Start_Time
noeud 5, que le Start_Time
noeud 5 est inférieur au Finish_Time
noeud 4 et que les deux Id
valeurs ne correspondent pas.
- Le Gather Streams (arrêter l'échange de parallélisme) au nœud 1 fusionne les flux ordonnés de chaque thread jusqu'à ce que 100 lignes aient été produites. La nature préservant l'ordre de la fusion sur plusieurs flux est ce qui nécessite la ligne supplémentaire mentionnée à l'étape 5.
La grande inefficacité se situe évidemment aux étapes 6 et 7 ci-dessus. L'analyse complète de la table au nœud 5 pour chaque itération n'est que légèrement raisonnable si elle ne se produit que 94 fois comme le prévoit l'optimiseur. L'ensemble de comparaisons d'environ 38 millions de lignes au nœud 2 représente également un coût élevé.
Surtout, l'estimation de l'objectif de la ligne 93/94 est également très probablement erronée, car elle dépend de la distribution des valeurs. L'optimiseur suppose une distribution uniforme en l'absence d'informations plus détaillées. En termes simples, cela signifie que si 1% des lignes du tableau sont censées se qualifier, l'optimiseur explique que pour trouver 1 ligne correspondante, il doit lire 100 lignes.
Si vous exécutez cette requête jusqu'à la fin (ce qui peut prendre très longtemps), vous constaterez très probablement que plus de 93/94 lignes doivent être lues à partir du tri afin de produire finalement 100 lignes. Dans le pire des cas, la 100e ligne serait trouvée en utilisant la dernière ligne du tri. En supposant que l'estimation de l'optimiseur au nœud 4 est correcte, cela signifie exécuter le scan au nœud 5 38 284 fois, pour un total d'environ 15 milliards de lignes. Cela pourrait être plus si les estimations de scan sont également désactivées.
Ce plan d'exécution comprend également un avertissement d'index manquant:
/*
The Query Processor estimates that implementing the following index
could improve the query cost by 72.7096%.
WARNING: This is only an estimate, and the Query Processor is making
this recommendation based solely upon analysis of this specific query.
It has not considered the resulting index size, or its workload-wide
impact, including its impact on INSERT, UPDATE, DELETE performance.
These factors should be taken into account before creating this index.
*/
CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[Batch_Tasks_Queue] ([Operation_Type],[State],[Start_Time])
INCLUDE ([Id],[Parameters])
L'optimiseur vous alerte du fait que l'ajout d'un index à la table améliorerait les performances.
Planifiez sans les colonnes supplémentaires
Il s'agit essentiellement du même plan que le précédent, avec l'ajout de la bobine d'indexation au nœud 6 et du filtre au nœud 5. Les différences importantes sont les suivantes:
- La bobine d'index au nœud 6 est une bobine désireuse. Il consomme avec impatience le résultat de l'analyse en dessous, et crée un index temporaire avec
Operation_Type
et Start_Time
avec, Id
comme colonne non clé.
- La jointure de boucles imbriquées au nœud 2 est désormais une jointure d'index. Aucune prédicats de jointure sont évaluées ici, au lieu les par itération les valeurs actuelles de
Operation_Type
, Start_Time
, Finish_Time
et Id
de l'analyse au niveau du noeud 4 sont transmises à la branche du côté interne comme références externes.
- L'analyse au nœud 7 n'est effectuée qu'une seule fois.
- La bobine d'indexation au nœud 6 recherche des lignes de l'index temporaire où
Operation_Type
correspond à la valeur de référence externe actuelle, et le Start_Time
est dans la plage définie par les références externes Start_Time
et Finish_Time
.
- Le filtre au nœud 5 teste les
Id
valeurs de la bobine d'indexation pour rechercher l'inégalité par rapport à la valeur de référence externe actuelle de Id
.
Les principales améliorations sont les suivantes:
- Le scan du côté intérieur n'est effectué qu'une seule fois
- Un index temporaire sur (
Operation_Type
, Start_Time
) avec Id
comme colonne incluse permet une jointure de boucles imbriquées d'index. L'index est utilisé pour rechercher des lignes correspondantes à chaque itération plutôt que d'analyser la table entière à chaque fois.
Comme précédemment, l'optimiseur inclut un avertissement concernant un index manquant:
/*
The Query Processor estimates that implementing the following index
could improve the query cost by 24.1475%.
WARNING: This is only an estimate, and the Query Processor is making
this recommendation based solely upon analysis of this specific query.
It has not considered the resulting index size, or its workload-wide
impact, including its impact on INSERT, UPDATE, DELETE performance.
These factors should be taken into account before creating this index.
*/
CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[Batch_Tasks_Queue] ([State],[Start_Time])
INCLUDE ([Id],[Operation_Type])
GO
Conclusion
Le plan sans les colonnes supplémentaires est plus rapide car l'optimiseur a choisi de créer un index temporaire pour vous.
Le plan avec les colonnes supplémentaires rendrait l'index temporaire plus coûteux à construire. La [Parameters
colonne] est nvarchar(2000)
, ce qui ajouterait jusqu'à 4000 octets à chaque ligne de l'index. Le coût supplémentaire suffit pour convaincre l'optimiseur que la construction de l'index temporaire à chaque exécution ne serait pas rentable.
L'optimiseur avertit dans les deux cas qu'un index permanent serait une meilleure solution. La composition idéale de l'index dépend de votre charge de travail plus large. Pour cette requête particulière, les index suggérés sont un point de départ raisonnable, mais vous devez comprendre les avantages et les coûts impliqués.
Recommandation
Un large éventail d'index possibles serait bénéfique pour cette requête. Le point important à retenir est qu'une sorte d'index non clusterisé est nécessaire. D'après les informations fournies, un indice raisonnable serait à mon avis:
CREATE NONCLUSTERED INDEX i1
ON dbo.Batch_Tasks_Queue (Start_Time DESC)
INCLUDE (Operation_Type, [State], Finish_Time);
Je serais également tenté d'organiser un peu mieux la requête et de retarder la recherche des [Parameters]
colonnes larges dans l'index cluster jusqu'à ce que les 100 premières lignes aient été trouvées (en utilisant Id
comme clé):
SELECT TOP (100)
BTQ1.id,
BTQ2.id,
BTQ3.[Parameters],
BTQ4.[Parameters]
FROM dbo.Batch_Tasks_Queue AS BTQ1
JOIN dbo.Batch_Tasks_Queue AS BTQ2 WITH (FORCESEEK)
ON BTQ2.Operation_Type = BTQ1.Operation_Type
AND BTQ2.Start_Time > BTQ1.Start_Time
AND BTQ2.Start_Time < BTQ1.Finish_Time
AND BTQ2.id != BTQ1.id
-- Look up the [Parameters] values
JOIN dbo.Batch_Tasks_Queue AS BTQ3
ON BTQ3.Id = BTQ1.Id
JOIN dbo.Batch_Tasks_Queue AS BTQ4
ON BTQ4.Id = BTQ2.Id
WHERE
BTQ1.[State] IN (3, 4)
AND BTQ2.[State] IN (3, 4)
AND BTQ1.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
AND BTQ2.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
-- These predicates are not strictly needed
AND BTQ1.Start_Time IS NOT NULL
AND BTQ2.Start_Time IS NOT NULL
ORDER BY
BTQ1.Start_Time DESC;
Lorsque les [Parameters]
colonnes ne sont pas nécessaires, la requête peut être simplifiée pour:
SELECT TOP (100)
BTQ1.id,
BTQ2.id
FROM dbo.Batch_Tasks_Queue AS BTQ1
JOIN dbo.Batch_Tasks_Queue AS BTQ2 WITH (FORCESEEK)
ON BTQ2.Operation_Type = BTQ1.Operation_Type
AND BTQ2.Start_Time > BTQ1.Start_Time
AND BTQ2.Start_Time < BTQ1.Finish_Time
AND BTQ2.id != BTQ1.id
WHERE
BTQ1.[State] IN (3, 4)
AND BTQ2.[State] IN (3, 4)
AND BTQ1.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
AND BTQ2.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
AND BTQ1.Start_Time IS NOT NULL
AND BTQ2.Start_Time IS NOT NULL
ORDER BY
BTQ1.Start_Time DESC;
L' FORCESEEK
astuce est là pour vous assurer que l'optimiseur choisit un plan de boucles imbriquées indexées (il y a une tentation basée sur le coût pour l'optimiseur de sélectionner un hachage ou (plusieurs-plusieurs) jointures de fusion sinon, ce qui a tendance à ne pas bien fonctionner avec ce type de dans la pratique. Les deux se retrouvent avec des résidus importants; de nombreux éléments par compartiment dans le cas du hachage et de nombreux rembobinages pour la fusion).
Alternative
Si la requête (y compris ses valeurs spécifiques) était particulièrement critique pour les performances de lecture, je considérerais plutôt deux index filtrés:
CREATE NONCLUSTERED INDEX i1
ON dbo.Batch_Tasks_Queue (Start_Time DESC)
INCLUDE (Operation_Type, [State], Finish_Time)
WHERE
Start_Time IS NOT NULL
AND [State] IN (3, 4)
AND Operation_Type <> 23
AND Operation_Type <> 24
AND Operation_Type <> 25
AND Operation_Type <> 26
AND Operation_Type <> 27
AND Operation_Type <> 28
AND Operation_Type <> 30;
CREATE NONCLUSTERED INDEX i2
ON dbo.Batch_Tasks_Queue (Operation_Type, [State], Start_Time)
WHERE
Start_Time IS NOT NULL
AND [State] IN (3, 4)
AND Operation_Type <> 23
AND Operation_Type <> 24
AND Operation_Type <> 25
AND Operation_Type <> 26
AND Operation_Type <> 27
AND Operation_Type <> 28
AND Operation_Type <> 30;
Pour la requête qui n'a pas besoin de la [Parameters]
colonne, le plan estimé à l'aide des index filtrés est:
L'analyse d'index renvoie automatiquement toutes les lignes éligibles sans évaluer de prédicats supplémentaires. Pour chaque itération de la jointure de boucles imbriquées d'index, la recherche d'index effectue deux opérations de recherche:
- Un préfixe de recherche correspond sur
Operation_Type
et State
= 3, puis recherche la plage de Start_Time
valeurs, prédicat résiduel sur l' Id
inégalité.
- Un préfixe de recherche correspond à
Operation_Type
et State
= 4, puis recherche la plage de Start_Time
valeurs, prédicat résiduel sur l' Id
inégalité.
Lorsque la [Parameters]
colonne est nécessaire, le plan de requête ajoute simplement un maximum de 100 recherches singleton pour chaque table:
En guise de note finale, vous devriez envisager d'utiliser les types d'entiers standard intégrés plutôt que le numeric
cas échéant.