GROUP BY mit MAX versus nur MAX

8

Ich bin ein Programmierer, der sich mit einem großen Tisch befasst, der das folgende Schema hat:

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

Es gibt einen Clustered-Index für Name, UpdateTime

Ich habe mich gefragt, was schneller sein soll:

SELECT MAX(UpdateTime)
FROM [MyTable]

oder

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

Die Einfügungen in diese Tabelle befinden sich in Blöcken von 50.000 Zeilen mit demselben Datum . Daher dachte ich, eine Gruppierung nach könnte die MAXBerechnung vereinfachen .

Anstatt zu versuchen, maximal 150.000 Zeilen zu finden, wäre die Gruppierung nach bis zu 3 Zeilen und die Berechnung von MAXschneller? Ist meine Annahme richtig oder ist die Gruppierung nach auch kostspielig?

Ofiris
quelle

Antworten:

12

Ich habe die Tabelle big_table gemäß Ihrem Schema erstellt

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

Ich habe dann die Tabelle mit 50.000 Zeilen mit diesem Code gefüllt:

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

Mit SSMS habe ich dann beide Abfragen getestet und festgestellt, dass Sie in der ersten Abfrage nach dem MAX von TheData und in der zweiten nach dem MAX der Aktualisierungszeit suchen

Ich habe daher die erste Abfrage geändert, um auch die maximale Aktualisierungszeit zu erhalten

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

Mit Statistics Time erhalte ich die Anzahl der Millisekunden zurück, die zum Parsen, Kompilieren und Ausführen jeder Anweisung erforderlich sind

Mit Statistics IO erhalte ich Informationen zur Festplattenaktivität zurück

STATISTICS TIME und STATISTICS IO bieten nützliche Informationen. Zum Beispiel wurden temporäre Tabellen verwendet (angezeigt durch Arbeitstisch). Außerdem wurde angegeben, wie viele logische Seiten gelesen wurden, was die Anzahl der aus dem Cache gelesenen Datenbankseiten angibt.

Ich aktiviere dann den Ausführungsplan mit STRG + M (aktiviert den aktuellen Ausführungsplan anzeigen) und führe ihn dann mit F5 aus.

Dies bietet einen Vergleich beider Abfragen.

Hier ist die Ausgabe der Registerkarte Nachrichten

- Abfrage 1

Tabelle 'big_table'. Scananzahl 1, logische Lesevorgänge 543 , physische Lesevorgänge 0, Vorauslesevorgänge 0, Lob-Lesevorgänge 0, Lob-Lesevorgänge 0, Lobvorlesevorgänge 0.

SQL Server-Ausführungszeiten: CPU-Zeit = 16 ms, verstrichene Zeit = 6 ms .

- Abfrage 2

Tabelle ' Arbeitstisch '. Scananzahl 0, logische Lesevorgänge 0, physische Lesevorgänge 0, Vorauslesevorgänge 0, Lob-Lesevorgänge 0, Lob-Lesevorgänge 0, Lobvorlesevorgänge 0.

Tabelle 'big_table'. Scananzahl 1, logische Lesevorgänge 543 , physische Lesevorgänge 0, Vorauslesevorgänge 0, Lob-Lesevorgänge 0, Lob-Lesevorgänge 0, Lobvorlesevorgänge 0.

SQL Server-Ausführungszeiten: CPU-Zeit = 0 ms, verstrichene Zeit = 35 ms .

Beide Abfragen führen zu 543 logischen Lesevorgängen, aber die zweite Abfrage hat eine verstrichene Zeit von 35 ms, während die erste nur 6 ms hat. Sie werden auch feststellen, dass die zweite Abfrage zur Verwendung temporärer Tabellen in tempdb führt, die durch das Wort Arbeitstabelle angezeigt werden . Obwohl alle Werte für den Arbeitstisch auf 0 liegen, wurde in tempdb noch gearbeitet.

Dann gibt es die Ausgabe von der Registerkarte " Ausführungsplan" neben der Registerkarte "Nachrichten"

Geben Sie hier die Bildbeschreibung ein

Gemäß dem von MSSQL bereitgestellten Ausführungsplan hat die zweite von Ihnen bereitgestellte Abfrage Gesamtstapelkosten von 64%, während die erste nur 36% des Gesamtstapels kostet, sodass die erste Abfrage weniger Arbeit erfordert.

Mit SSMS können Sie Ihre Abfragen testen und vergleichen und genau herausfinden, wie MSSQL Ihre Abfragen analysiert und welche Objekte: Tabellen, Indizes und / oder Statistiken, falls vorhanden, um diese Abfragen zu erfüllen.

Eine zusätzliche Randnotiz, die Sie beim Testen beachten sollten, ist das Löschen des Caches vor dem Testen, wenn möglich. Dies hilft sicherzustellen, dass die Vergleiche korrekt sind, und dies ist wichtig, wenn Sie über die Festplattenaktivität nachdenken. Ich beginne mit DBCC DROPCLEANBUFFERS und DBCC FREEPROCCACHE , um den gesamten Cache zu leeren . Achten Sie jedoch darauf, diese Befehle nicht auf einem tatsächlich verwendeten Produktionsserver zu verwenden, da Sie den Server effektiv dazu zwingen, alles von der Festplatte in den Speicher zu lesen.

Hier ist die relevante Dokumentation.

  1. Leeren Sie den Plan-Cache mit DBCC FREEPROCCACHE
  2. Löschen Sie mit DBCC DROPCLEANBUFFERS alles aus dem Pufferpool

Die Verwendung dieser Befehle ist möglicherweise nicht möglich, je nachdem, wie Ihre Umgebung verwendet wird.

Aktualisiert 28.10. 12:46 Uhr

Das Ausführungsplan-Image und die Statistikausgabe wurden korrigiert.

Craig Efrein
quelle
Vielen Dank für die tiefe Antwort. Bitte beachten Sie meine kahle Linie im Code. Jede Gruppe von 50.000 Zeilen hat das gleiche Datum, das sich von anderen Blöcken unterscheidet. Also sollte getdate()ich die Schleife verlassen
Ofiris
1
Hallo @Ofiris. Die Antwort, die ich gegeben habe, ist eigentlich nur, um Ihnen zu helfen, den Vergleich selbst durchzuführen. Ich habe zufällige Junk-Daten erstellt, um die Verwendung der verschiedenen Befehle und Tools zu veranschaulichen, mit denen Sie zu Ihren eigenen Schlussfolgerungen gelangen können.
Craig Efrein
1
In tempdb wurde keine Arbeit geleistet. Die Arbeitstabelle dient zum Verwalten von Partitionen für den Fall, dass das Hash-Aggregat auf Tempdb übertragen werden muss, weil nicht genügend Speicher dafür reserviert wurde. Bitte betonen Sie, dass die Kosten auch in einem „tatsächlichen“ Plan immer geschätzt werden . Dies sind die Schätzungen des Optimierers, die möglicherweise nicht viel mit der tatsächlichen Leistung zu tun haben. Verwenden Sie% der Charge nicht als primäre Abstimmungsmetrik. Das Löschen von Puffern ist nur wichtig, wenn Sie die Leistung des Cold-Cache testen möchten.
Paul White 9
1
Hallo @PaulWhite. Vielen Dank für die zusätzlichen Informationen. Ich freue mich sehr über Vorschläge, wie man genauer sein kann. Wenn Sie Ihre Sätze jedoch so formulieren: "Nicht verwenden", kann dies nicht als Auftragserteilung interpretiert werden, anstatt professionelle Beratung anzubieten? Freundliche Grüße.
Craig Efrein
@CraigEfrein Wahrscheinlich. Ich war kurz davor, in den erlaubten Kommentarbereich zu passen.
Paul White 9
6

Die Einfügungen in diese Tabelle befinden sich in Blöcken von 50.000 Zeilen mit demselben Datum. Daher dachte ich, eine Gruppierung nach könnte die MAX-Berechnung vereinfachen.

Das Umschreiben hat möglicherweise geholfen, wenn SQL Server den Index-Skip-Scan implementiert hat, dies ist jedoch nicht der Fall.

Mit dem Index-Skip-Scan kann ein Datenbankmodul nach dem nächsthöheren Indexwert suchen, anstatt alle Duplikate (oder irrelevanten Unterschlüssel) dazwischen zu scannen. In Ihrem Fall würde das Überspringen des Scans es der Engine ermöglichen, MAX(UpdateTime)das erste zu finden Name, zum MAX(UpdateTime)zweiten zu Namespringen ... und so weiter. Der letzte Schritt wäre, die MAX(UpdateTime)Kandidaten aus einem Namen zu finden .

Sie können dies bis zu einem gewissen Grad mit einem rekursiven CTE simulieren, aber es ist etwas chaotisch und nicht so effizient wie ein integrierter Skip-Scan:

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);

Rekursiver CTE-Plan

Dieser Plan führt eine Singleton-Suche für jeden einzelnen Namedurch und findet dann den höchsten UpdateTimeunter den Kandidaten. Die Leistung im Vergleich zu einem einfachen vollständigen Scan der Tabelle hängt davon ab, wie viele Duplikate pro vorhanden Namesind und ob sich die von den Singleton-Suchvorgängen berührten Seiten im Speicher befinden oder nicht.

Alternativlösungen

Wenn Sie in der Lage sind, einen neuen Index für diese Tabelle zu erstellen, ist eine gute Wahl für diese Abfrage ein Index für UpdateTime:

CREATE INDEX IX__BigTable_UpdateTime 
ON dbo.BigTable (UpdateTime);

Dieser Index ermöglicht es der Ausführungs-Engine, UpdateTimemit einer Singleton-Suche bis zum Ende des Index-B-Baums den höchsten Wert zu finden :

Neuer Indexplan

Dieser Plan benötigt nur wenige logische E / A-Vorgänge (um durch die B-Tree-Ebenen zu navigieren) und wird sofort abgeschlossen. Beachten Sie, dass der Index-Scan im Plan kein vollständiger Scan des neuen Index ist - er gibt lediglich eine Zeile vom einen Ende des Index zurück.

Wenn Sie keinen vollständig neuen Index für die Tabelle erstellen möchten, können Sie eine indizierte Ansicht in Betracht ziehen, die nur die eindeutigen UpdateTimeWerte enthält:

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);

Dies hat den Vorteil, dass nur eine Struktur mit so vielen Zeilen erstellt wird, wie eindeutige UpdateTimeWerte vorhanden sind. Bei jeder Abfrage, die Daten in der Basistabelle ändert, werden dem Ausführungsplan zusätzliche Operatoren hinzugefügt, um die indizierte Ansicht beizubehalten. Die Abfrage zum Finden des Maximalwerts UpdateTimewäre:

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

Indizierter Ansichtsplan

Paul White 9
quelle