web-dev-qa-db-fra.com

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

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

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 a qu'un CreatedDate dans ce cas, donc la requête groupée renvoie 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.

Query1 Execution PlanQuery2 Execution Plan

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 clause GROUP BY?

Modifier

Je viens d'apprendre que je peux utiliser OPTION(querytraceon 8649) pour définir le coût supplémentaire 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 à en utilisant cet indice de requête.

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

enter image description here

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. Pour le moment, je termine 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 à demande de Martin pour plus d'informations :

CreatedDate et SomeIndexedValue ont un index non unique et non cluster séparé. SomeIndexedValue est 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.

MyTable contient 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

12
Rachel

Il semble qu'il suive probablement un index sur CreatedDate dans l'ordre du plus bas au plus haut et effectue des recherches pour évaluer le SomeIndexedValue = 1 pré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 sur SomeIndexedValue, CreatedDate. En supposant que vous ne pouvez pas ajouter cela ou au moins créer votre index existant sur SomeIndexedValue couvrir CreatedDate en tant 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.

8
Martin Smith

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 y arriver) à 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 atteindriez 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
2
ooutwire

À 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.

0
yoel halb