Pourquoi une requête agrégée est-elle beaucoup plus rapide avec une clause GROUP BY que sans une?


12

Je suis simplement curieux de savoir pourquoi une requête agrégée s'exécute tellement plus rapidement avec une GROUP BYclause que sans.

Par exemple, l'exécution de cette requête prend près de 10 secondes

SELECT MIN(CreatedDate)
FROM MyTable
WHERE SomeIndexedValue = 1

Alors que celui-ci prend moins d'une seconde

SELECT MIN(CreatedDate)
FROM MyTable
WHERE SomeIndexedValue = 1
GROUP BY CreatedDate

Il n'y en a qu'un CreatedDatedans ce cas, la requête groupée renvoie donc les mêmes résultats que la requête non groupée.

J'ai remarqué que les plans d'exécution des deux requêtes sont différents - La deuxième requête utilise le parallélisme alors que la première ne le fait pas.

Plan d'exécution de la requête 1 Plan d'exécution de la requête 2

Est-il normal que SQL Server évalue une requête agrégée différemment s'il n'a pas de clause GROUP BY? Et puis-je faire quelque chose pour améliorer les performances de la première requête sans utiliser de GROUP BYclause?

Éditer

Je viens d'apprendre que je peux utiliser OPTION(querytraceon 8649)pour définir les frais généraux du parallélisme à 0, ce qui oblige la requête à utiliser un certain parallélisme et réduit le temps d'exécution à 2 secondes, bien que je ne sache pas s'il y a des inconvénients à utiliser cet indice de requête.

SELECT MIN(CreatedDate)
FROM MyTable
WHERE SomeIndexedValue = 1
OPTION(querytraceon 8649)

entrez la description de l'image ici

Je préférerais toujours un temps d'exécution plus court car la requête est destinée à remplir une valeur lors de la sélection de l'utilisateur, donc devrait idéalement être instantanée comme la requête groupée. En ce moment, je suis juste en train de terminer ma requête, mais je sais que ce n'est pas vraiment une solution idéale.

SELECT Min(CreatedDate)
FROM
(
    SELECT Min(CreatedDate) as CreatedDate
    FROM MyTable WITH (NOLOCK) 
    WHERE SomeIndexedValue = 1
    GROUP BY CreatedDate
) as T

Éditer # 2

En réponse à la demande de Martin pour plus d'informations :

Les deux CreatedDateet SomeIndexedValueont un index non unique et non cluster séparé sur eux. SomeIndexedValueest en fait un champ varchar (7), même s'il stocke une valeur numérique qui pointe vers le PK (int) d'une autre table. La relation entre les deux tables n'est pas définie dans la base de données. Je ne suis pas du tout censé changer la base de données et je ne peux écrire que des requêtes qui interrogent des données.

MyTablecontient plus de 3 millions d'enregistrements, et à chaque enregistrement est affecté un groupe auquel il appartient ( SomeIndexedValue). Les groupes peuvent contenir de 1 à 200 000 enregistrements

Réponses:


8

Il semble qu'il suive probablement un index CreatedDatedans l'ordre du plus bas au plus haut et effectue des recherches pour évaluer le SomeIndexedValue = 1prédicat.

Lorsqu'il trouve la première ligne correspondante, cela est fait, mais il se peut bien qu'il effectue beaucoup plus de recherches qu'il ne l'attend avant de trouver une telle ligne (il suppose que les lignes correspondant au prédicat sont distribuées de manière aléatoire en fonction de la date.)

Voir ma réponse ici pour un problème similaire

L'index idéal pour cette requête serait un SomeIndexedValue, CreatedDate. En supposant que vous ne pouvez pas ajouter cela ou au moins créer votre index existant en SomeIndexedValuecouverture en CreatedDatetant que colonne incluse, vous pouvez essayer de réécrire la requête comme suit

SELECT MIN(DATEADD(DAY, 0, CreatedDate)) AS CreatedDate
FROM MyTable
WHERE SomeIndexedValue = 1

pour l'empêcher d'utiliser ce plan particulier.


2

Pouvons-nous contrôler MAXDOP et choisir une table connue, par exemple AdventureWorks.Production.TransactionHistory?

Lorsque je répète votre configuration en utilisant

--#1
SELECT MIN(TransactionDate) 
FROM AdventureWorks.Production.TransactionHistory
WHERE TransactionID = 100001 
OPTION( MAXDOP 1) ;

--#2
SELECT MIN(TransactionDate) 
FROM AdventureWorks.Production.TransactionHistory
WHERE TransactionID = 100001 
GROUP BY TransactionDate
OPTION( MAXDOP 1) ;
GO 

les coûts sont identiques.

En passant, je m'attendrais (pour que cela se produise) à une recherche d'index sur votre valeur indexée; sinon, vous verrez probablement des correspondances de hachage au lieu d'agrégats de flux. Vous pouvez améliorer les performances avec des index non clusterisés qui incluent les valeurs que vous agrégez et ou créer une vue indexée qui définit vos agrégats sous forme de colonnes. Ensuite, vous frapperiez un index cluster, qui contient vos agrégations, par un ID indexé. Dans SQL Standard, vous pouvez simplement créer la vue et utiliser l'indicateur WITH (NOEXPAND).

Un exemple (je n'utilise pas MIN, car il ne fonctionne pas dans les vues indexées):

USE AdventureWorks ;
GO

-- Covering Index with Include
CREATE INDEX IX_CoverAndInclude
ON Production.TransactionHistory(TransactionDate) 
INCLUDE (Quantity) ;
GO

-- Indexed View
CREATE VIEW dbo.SumofQtyByTransDate
    WITH SCHEMABINDING
AS
SELECT 
      TransactionDate 
    , COUNT_BIG(*) AS NumberOfTransactions
    , SUM(Quantity) AS TotalTransactions
FROM Production.TransactionHistory
GROUP BY TransactionDate ;
GO

CREATE UNIQUE CLUSTERED INDEX SumofAllChargesIndex 
    ON dbo.SumofQtyByTransDate (TransactionDate) ;  
GO


--#1
SELECT SUM(Quantity) 
FROM AdventureWorks.Production.TransactionHistory 
WITH (INDEX(0))
WHERE TransactionID = 100001 
OPTION( MAXDOP 1) ;

--#2
SELECT SUM(Quantity)  
FROM AdventureWorks.Production.TransactionHistory 
WITH (INDEX(IX_CoverAndInclude))
WHERE TransactionID = 100001 
GROUP BY TransactionDate
OPTION( MAXDOP 1) ;
GO 

--#3
SELECT SUM(Quantity)  
FROM AdventureWorks.Production.TransactionHistory
WHERE TransactionID = 100001 
GROUP BY TransactionDate
OPTION( MAXDOP 1) ;
GO

MAXDOPdéfinit le degré maximal de parallélisme, ce qui limite le nombre de processeurs que la requête peut utiliser. Cela rendrait la deuxième requête aussi lente que la première, car elle supprime ses capacités à utiliser le parallélisme, ce qui n'est pas ce que je veux.
Rachel

@Rachel je suis d'accord; mais nous ne pouvons rien comparer à moins de fixer des règles de base. Je ne peux pas facilement comparer un processus parallèle exécuté sur 64 cœurs à un seul thread exécuté sur un. Au final, j'espère que toutes nos machines ont au moins un CPU logique = -)
ooutwire

0

À mon avis, la raison du problème est que l'optimiseur de serveur SQL ne recherche pas le meilleur plan, mais plutôt un bon plan, comme le montre le fait qu'après avoir forcé le parallélisme, la requête s'est exécutée beaucoup plus rapidement, ce que l'optimiseur avait pas fait tout seul.

J'ai également vu de nombreuses situations où la réécriture de la requête dans un format différent faisait la différence entre la parallélisation (par exemple, bien que la plupart des articles sur SQL recommandent le paramétrage, j'ai trouvé que cela entraînait parfois noy pour paralléliser même lorsque les paramètres reniflés étaient les mêmes qu'un non - une parallélisation, ou la combinaison de deux requêtes avec UNION ALL peut parfois éliminer la parallélisation).

En tant que telle, la bonne solution pourrait être d'essayer différentes manières d'écrire la requête, telles que les tables temporaires, les variables de table, le cte, les tables dérivées, le paramétrage, etc., et également jouer avec les index, les vues indexées ou les index filtrés dans afin d'obtenir le meilleur plan.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.