GROUP BY avec MAX contre seulement MAX

8

Je suis un programmeur, traitant d'une grande table dont le schéma suivant:

UpdateTime, PK, datetime, notnull
Name, PK, char(14), notnull
TheData, float

Il existe un index clusterisé sur Name, UpdateTime

Je me demandais ce qui devrait être plus rapide:

SELECT MAX(UpdateTime)
FROM [MyTable]

ou

SELECT MAX([UpdateTime]) AS value
from
   (
    SELECT [UpdateTime]
    FROM [MyTable]
    group by [UpdateTime]
   ) as t

Les insertions de ce tableau sont en blocs de 50 000 lignes avec la même date . J'ai donc pensé que le regroupement par pourrait faciliter le MAXcalcul.

Au lieu d'essayer de trouver un maximum de 150 000 lignes, le regroupement en 3 lignes, puis le calcul de MAXserait plus rapide? Mon hypothèse est-elle correcte ou regrouper est-elle également coûteuse?

Ofiris
la source

Réponses:

12

J'ai créé la table big_table selon votre schéma

create table big_table
(
    updatetime datetime not null,
    name char(14) not null,
    TheData float,
    primary key(Name,updatetime)
)

J'ai ensuite rempli le tableau avec 50 000 lignes avec ce code:

DECLARE @ROWNUM as bigint = 1
WHILE(1=1)
BEGIN
    set @rownum  = @ROWNUM + 1
    insert into big_table values(getdate(),'name' + cast(@rownum as CHAR), cast(@rownum as float))
    if @ROWNUM > 50000
        BREAK;  
END

À l'aide de SSMS, j'ai ensuite testé les deux requêtes et réalisé que dans la première requête, vous recherchez le MAX de TheData et dans la seconde, le MAX de la mise à jour

J'ai donc modifié la première requête pour obtenir également le MAX de mise à jour

set statistics time on -- execution time
set statistics io on -- io stats (how many pages read, temp tables)

-- query 1
SELECT MAX([UpdateTime])
FROM big_table

-- query 2
SELECT MAX([UpdateTime]) AS value
from
   (
    SELECT [UpdateTime]
    FROM big_table
    group by [UpdateTime]
   ) as t


set statistics time off
set statistics io off

En utilisant Statistics Time, je récupère le nombre de millisecondes nécessaires pour analyser, compiler et exécuter chaque instruction

En utilisant Statistics IO, je récupère des informations sur l'activité du disque

STATISTICS TIME et STATISTICS IO fournissent des informations utiles. Tels que les tables temporaires utilisées (indiquées par la table de travail). Le nombre de pages logiques lues a également été lu, ce qui indique le nombre de pages de base de données lues dans le cache.

J'active ensuite le plan d'exécution avec CTRL + M (active afficher le plan d'exécution réel), puis j'exécute avec F5.

Cela fournira une comparaison des deux requêtes.

Voici la sortie de l' onglet Messages

- Requête 1

Table 'big_table'. Nombre de balayages 1, lectures logiques 543 , lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0.

Temps d'exécution SQL Server: temps CPU = 16 ms, temps écoulé = 6 ms .

- Requête 2

Table ' table de travail . Nombre de balayages 0, lectures logiques 0, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0.

Table 'big_table'. Nombre de balayages 1, lectures logiques 543 , lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0.

Temps d'exécution SQL Server: temps CPU = 0 ms, temps écoulé = 35 ms .

Les deux requêtes entraînent 543 lectures logiques, mais la deuxième requête a un temps écoulé de 35 ms alors que la première n'a que 6 ms. Vous remarquerez également que la deuxième requête entraîne l'utilisation de tables temporaires dans tempdb, indiquées par le mot table de travail . Même si toutes les valeurs de la table de travail sont à 0, le travail a toujours été effectué dans tempdb.

Ensuite, il y a la sortie de l' onglet Plan d'exécution réel à côté de l'onglet Messages

entrez la description de l'image ici

Selon le plan d'exécution fourni par MSSQL, la deuxième requête que vous avez fournie a un coût total par lot de 64% tandis que la première ne coûte que 36% du lot total, donc la première requête nécessite moins de travail.

À l'aide de SSMS, vous pouvez tester et comparer vos requêtes et savoir exactement comment MSSQL analyse vos requêtes et quels objets: tables, index et / ou statistiques, le cas échéant, sont utilisés pour satisfaire ces requêtes.

Une remarque supplémentaire à garder à l'esprit lors du test consiste à nettoyer le cache avant le test, si possible. Cela permet de garantir l'exactitude des comparaisons, ce qui est important lorsque vous pensez à l'activité du disque. Je commence par DBCC DROPCLEANBUFFERS et DBCC FREEPROCCACHE pour vider tout le cache. Attention cependant à ne pas utiliser ces commandes sur un serveur de production réellement utilisé car vous forcerez effectivement le serveur à tout lire du disque dans la mémoire.

Voici la documentation pertinente.

  1. Vider le cache du plan avec DBCC FREEPROCCACHE
  2. Effacez tout du pool de tampons avec DBCC DROPCLEANBUFFERS

L'utilisation de ces commandes peut ne pas être possible selon la façon dont votre environnement est utilisé.

Mis à jour 10/28 12:46 pm

Correction de l'image du plan d'exécution et de la sortie des statistiques.

Craig Efrein
la source
Merci pour la réponse profonde, veuillez noter ma ligne chauve dans le code, chaque groupe de 50 000 lignes a la même date qui est différente des autres morceaux. Je devrais donc getdate()sortir de la boucle
Ofiris
1
Bonjour @Ofiris. La réponse que j'ai donnée est en fait juste pour vous aider à faire la comparaison par vous-même. J'ai créé des données indésirables aléatoires juste pour illustrer l'utilisation des différentes commandes et outils que vous pouvez utiliser pour tirer vos propres conclusions.
Craig Efrein
1
Aucun travail n'a été effectué dans tempdb. La table de travail consiste à gérer les partitions au cas où l'agrégat de hachage doit se déverser dans tempdb car une mémoire insuffisante lui était réservée. Veuillez souligner que les coûts sont toujours des estimations même dans un plan «réel». Ce sont les estimations de l'optimiseur, qui peuvent ne pas être très liées aux performances réelles. N'utilisez pas% du lot comme métrique de réglage principale. L'effacement des tampons n'est important que si vous souhaitez tester les performances du cache froid.
Paul White 9
1
Bonjour @PaulWhite. Merci pour les informations supplémentaires, j'apprécie sincèrement toutes les suggestions sur la façon d'être plus précis. Cependant, lorsque vous formulez vos phrases: "Ne pas utiliser", cela ne pourrait-il pas être interprété à tort comme donnant un ordre plutôt que d'offrir des conseils professionnels? Meilleures salutations.
Craig Efrein
@CraigEfrein Probablement. J'étais bref pour tenir dans l'espace de commentaires autorisé.
Paul White 9
6

Les insertions de ce tableau sont en blocs de 50 000 lignes avec la même date. J'ai donc pensé que le regroupement par pourrait faciliter le calcul MAX.

La réécriture aurait peut-être aidé si SQL Server avait implémenté le saut d'index, mais ce n'est pas le cas.

Le saut d'index permet à un moteur de base de données de rechercher la prochaine valeur d'index différente au lieu d'analyser tous les doublons (ou sous-clés non pertinents) entre les deux. Dans votre cas, skip-scan permettrait au moteur de trouver MAX(UpdateTime)le premier Name, de passer au MAX(UpdateTime)second Name... et ainsi de suite. La dernière étape serait de trouver les MAX(UpdateTime)candidats un par nom.

Vous pouvez simuler cela dans une certaine mesure en utilisant un CTE récursif, mais c'est un peu désordonné, et pas aussi efficace que le skip-scan intégré serait:

WITH RecursiveCTE
AS
(
    -- Anchor: MAX UpdateTime for
    -- highest-sorting Name
    SELECT TOP (1)
        BT.Name,
        BT.UpdateTime
    FROM dbo.BigTable AS BT
    ORDER BY
        BT.Name DESC,
        BT.UpdateTime DESC

    UNION ALL

    -- Recursive part
    -- MAX UpdateTime for Name
    -- that sorts immediately lower
    SELECT
        SubQuery.Name,
        SubQuery.UpdateTime
    FROM 
    (
        SELECT
            BT.Name,
            BT.UpdateTime,
            rn = ROW_NUMBER() OVER (
                ORDER BY BT.Name DESC, BT.UpdateTime DESC)
        FROM RecursiveCTE AS R
        JOIN dbo.BigTable AS BT
            ON BT.Name < R.Name
    ) AS SubQuery
    WHERE
        SubQuery.rn = 1
)
-- Final MAX aggregate over
-- MAX(UpdateTime) per Name
SELECT MAX(UpdateTime) 
FROM RecursiveCTE
OPTION (MAXRECURSION 0);

Plan CTE récursif

Ce plan effectue une recherche de singleton pour chaque distinct Name, puis trouve le plus élevé UpdateTimeparmi les candidats. Ses performances par rapport à une simple analyse complète du tableau dépendent du nombre de doublons par Nameet du fait que les pages touchées par le singleton soient en mémoire ou non.

Solutions alternatives

Si vous pouvez créer un nouvel index sur cette table, un bon choix pour cette requête serait un index UpdateTimeseul:

CREATE INDEX IX__BigTable_UpdateTime 
ON dbo.BigTable (UpdateTime);

Cet index permettra au moteur d'exécution de trouver le plus haut UpdateTimeavec une recherche singleton à la fin de l'arbre b de l'index:

Nouveau plan d'index

Ce plan ne consomme que quelques E / S logiques (pour naviguer dans les niveaux de l'arborescence b) et se termine immédiatement. Notez que l'analyse d'index dans le plan n'est pas une analyse complète du nouvel index - elle renvoie simplement une ligne à partir de la «fin» de l'index.

Si vous ne souhaitez pas créer un nouvel index complet sur la table, vous pouvez envisager une vue indexée contenant uniquement les UpdateTimevaleurs uniques :

CREATE VIEW dbo.BigTableUpdateTimes
WITH SCHEMABINDING AS
SELECT 
    UpdateTime, 
    NumRows = COUNT_BIG(*)
FROM dbo.BigTable AS BT
GROUP BY
    UpdateTime;
GO
CREATE UNIQUE CLUSTERED INDEX cuq
ON dbo.BigTableUpdateTimes (UpdateTime);

Cela a l'avantage de créer uniquement une structure avec autant de lignes qu'il y a de UpdateTimevaleurs uniques , bien que chaque requête qui modifie des données dans la table de base aura des opérateurs supplémentaires ajoutés à son plan d'exécution pour maintenir la vue indexée. La requête pour trouver la UpdateTimevaleur maximale serait:

SELECT MAX(BTUT.UpdateTime)
FROM dbo.BigTableUpdateTimes AS BTUT
    WITH (NOEXPAND);

Plan de vue indexée

Paul White 9
la source