La requête ne répond pas lors de l'ajout de deux colonnes

9

Lorsque j'ajoute deux colonnes à ma sélection, la requête ne répond pas. Le type de colonne est nvarchar(2000). C'est un peu inhabituel.

  • La version de SQL Server est 2014.
  • Il n'y a qu'un seul index primaire.
  • L'ensemble des enregistrements ne contient que 1 000 lignes.

Voici le plan d'exécution avant ( showplan XML ):

entrez la description de l'image ici

Plan d'exécution après ( XML showplan ):

entrez la description de l'image ici

Voici la requête:

select top(100)
  Batch_Tasks_Queue.id,
  btq.id,
  Batch_Tasks_Queue.[Parameters], -- this field
  btq.[Parameters]  -- and this field
from
        Batch_Tasks_Queue with(nolock)
    inner join  Batch_Tasks_Queue btq with(nolock)  on  Batch_Tasks_Queue.Start_Time < btq.Start_Time
                            and btq.Start_Time < Batch_Tasks_Queue.Finish_Time
                            and Batch_Tasks_Queue.id <> btq.id                            
                            and btq.Start_Time is not null
                            and btq.State in (3, 4)                          
where
    Batch_Tasks_Queue.Start_Time is not null      
    and Batch_Tasks_Queue.State in (3, 4)
    and Batch_Tasks_Queue.Operation_Type = btq.Operation_Type
    and Batch_Tasks_Queue.Operation_Type not in (23, 24, 25, 26, 27, 28, 30)

order by
    Batch_Tasks_Queue.Start_Time desc

Le nombre total de résultats est de 17 lignes. Les données sales (indice nolock) ne sont pas importantes.

Voici la structure du tableau:

CREATE TABLE [dbo].[Batch_Tasks_Queue](
    [Id] [int] NOT NULL,
    [OBJ_VERSION] [numeric](8, 0) NOT NULL,
    [Operation_Type] [numeric](2, 0) NULL,
    [Request_Time] [datetime] NOT NULL,
    [Description] [varchar](1000) NULL,
    [State] [numeric](1, 0) NOT NULL,
    [Start_Time] [datetime] NULL,
    [Finish_Time] [datetime] NULL,
    [Parameters] [nvarchar](2000) NULL,
    [Response] [nvarchar](max) NULL,
    [Billing_UserId] [int] NOT NULL,
    [Planned_Start_Time] [datetime] NULL,
    [Input_FileId] [uniqueidentifier] NULL,
    [Output_FileId] [uniqueidentifier] NULL,
    [PRIORITY] [numeric](2, 0) NULL,
    [EXECUTE_SEQ] [numeric](2, 0) NULL,
    [View_Access] [numeric](1, 0) NULL,
    [Seeing] [numeric](1, 0) NULL,
 CONSTRAINT [PKBachTskQ] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [Batch_Tasks_QueueData]
) ON [Batch_Tasks_QueueData] TEXTIMAGE_ON [Batch_Tasks_QueueData]
GO    
SET ANSI_PADDING OFF
GO
ALTER TABLE [dbo].[Batch_Tasks_Queue]  WITH NOCHECK ADD  CONSTRAINT [FK0_BtchTskQ_BlngUsr] FOREIGN KEY([Billing_UserId])
REFERENCES [dbo].[BILLING_USER] ([ID])
GO
ALTER TABLE [dbo].[Batch_Tasks_Queue] CHECK CONSTRAINT [FK0_BtchTskQ_BlngUsr]
GO
Hamid Fathi
la source
La discussion sur cette question a été déplacée dans cette salle de discussion .
Paul White 9

Réponses:

15

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:

Plan lent

Les fonctionnalités intéressantes sont:

  1. 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.
  2. L'analyse au nœud 4 trouve des lignes de la table où le Start_Timen'est pas nul, Stateest 3 ou 4 et Operation_Typeest 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.
  3. 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.
  4. 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).
  5. 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.
  6. L'analyse au nœud 5 analyse entièrement la table à chaque itération. Il trouve des lignes où Start_Timen'est pas nul et Statevaut 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.
  7. La jointure des boucles imbriquées au nœud 2 applique également les prédicats de jointure. Il vérifie ce qui Operation_Typecorrespond, que le Start_Timenoeud 4 est inférieur au Start_Timenoeud 5, que le Start_Timenoeud 5 est inférieur au Finish_Timenoeud 4 et que les deux Idvaleurs ne correspondent pas.
  8. 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

Plan moins lent

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:

  1. 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_Typeet Start_Timeavec, Idcomme colonne non clé.
  2. 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_Timeet Idde l'analyse au niveau du noeud 4 sont transmises à la branche du côté interne comme références externes.
  3. L'analyse au nœud 7 n'est effectuée qu'une seule fois.
  4. La bobine d'indexation au nœud 6 recherche des lignes de l'index temporaire où Operation_Typecorrespond à la valeur de référence externe actuelle, et le Start_Timeest dans la plage définie par les références externes Start_Timeet Finish_Time.
  5. Le filtre au nœud 5 teste les Idvaleurs 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 Idcomme 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 [Parameterscolonne] 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 Idcomme 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' FORCESEEKastuce 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:

Plan d'indexation filtré simple

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:

  1. Un préfixe de recherche correspond sur Operation_Typeet State= 3, puis recherche la plage de Start_Timevaleurs, prédicat résiduel sur l' Idinégalité.
  2. Un préfixe de recherche correspond à Operation_Typeet State= 4, puis recherche la plage de Start_Timevaleurs, prédicat résiduel sur l' Idinégalité.

Lorsque la [Parameters]colonne est nécessaire, le plan de requête ajoute simplement un maximum de 100 recherches singleton pour chaque table:

Plan d'index filtré avec colonnes supplémentaires

En guise de note finale, vous devriez envisager d'utiliser les types d'entiers standard intégrés plutôt que le numericcas échéant.

Paul White 9
la source
-2

Veuillez créer l'index suivant:

create index Batch_Tasks_Queue_ix_Start_Time on Batch_Tasks_Queue(Start_Time);
David דודו Markovitz
la source
L'as tu essayé? J'ai effectué quelques tests et cela a fait une énorme différence.
David דודו Markovitz