Unterabfrage mit Fensterfunktion optimieren

8

Da sich meine Fähigkeiten zur Leistungsoptimierung nie ausreichend anfühlen, frage ich mich immer, ob ich bei einigen Abfragen mehr Optimierungen vornehmen kann. Die Situation, auf die sich diese Frage bezieht, ist eine Windowed MAX-Funktion, die in einer Unterabfrage verschachtelt ist.

Die Daten, die ich durchsuche, sind eine Reihe von Transaktionen für verschiedene Gruppen größerer Mengen. Ich habe 4 wichtige Felder, die eindeutige ID einer Transaktion, die Gruppen-ID eines Transaktionsstapels und Daten, die der jeweiligen eindeutigen Transaktion oder Gruppe von Transaktionen zugeordnet sind. In den meisten Fällen stimmt das Gruppendatum mit dem maximalen eindeutigen Transaktionsdatum für einen Stapel überein. Es gibt jedoch Zeiten, in denen manuelle Anpassungen über unser System vorgenommen werden und nach der Erfassung des Gruppentransaktionsdatums eine eindeutige Datumsoperation erfolgt. Diese manuelle Bearbeitung passt das Gruppendatum nicht an.

Was ich in dieser Abfrage identifiziere, sind die Datensätze, bei denen das eindeutige Datum nach dem Gruppendatum liegt. Die folgende Beispielabfrage liefert ein ungefähres Äquivalent zu meinem Szenario, und die SELECT-Anweisung gibt die gesuchten Datensätze zurück. Nähere ich mich dieser Lösung jedoch auf die effizienteste Weise? Es dauert eine Weile, bis meine Faktentabelle geladen ist, da mein Datensatz die Nummer in den oberen 9 Ziffern zählt, aber meistens frage ich mich, ob es hier einen besseren Ansatz gibt, wenn ich Unterabfragen verachte. Ich bin nicht so besorgt über Indizes, wie ich zuversichtlich bin, dass diese bereits vorhanden sind. Was ich suche, ist ein alternativer Abfrageansatz, der dasselbe erreicht, aber noch effizienter. Jedes Feedback ist willkommen.

CREATE TABLE #Example
(
    UniqueID INT IDENTITY(1,1)
  , GroupID INT
  , GroupDate DATETIME
  , UniqueDate DATETIME
)

CREATE CLUSTERED INDEX [CX_1] ON [#Example]
(
    [UniqueID] ASC
)


SET NOCOUNT ON

--Populate some test data
DECLARE @i INT = 0, @j INT = 5, @UniqueDate DATETIME, @GroupDate DATETIME

WHILE @i < 10000
BEGIN

    IF((@i + @j)%173 = 0)
    BEGIN
        SET @UniqueDate = GETDATE()+@i+5
    END
    ELSE
    BEGIN
        SET @UniqueDate = GETDATE()+@i
    END

    SET @GroupDate = GETDATE()+(@j-1)

    INSERT INTO #Example (GroupID, GroupDate, UniqueDate)
    VALUES (@j, @GroupDate, @UniqueDate)

    SET @i = @i + 1

    IF (@i % 5 = 0)
    BEGIN
        SET @j = @j+5
    END
END
SET NOCOUNT OFF

CREATE NONCLUSTERED INDEX [IX_2_4_3] ON [#Example]
(
    [GroupID] ASC,
    [UniqueDate] ASC,
    [GroupDate] ASC
)
INCLUDE ([UniqueID])

-- Identify any UniqueDates that are greater than the GroupDate within their GroupID
SELECT UniqueID
     , GroupID
     , GroupDate
     , UniqueDate
FROM (
    SELECT UniqueID
         , GroupID
         , GroupDate
         , UniqueDate
         , MAX(UniqueDate) OVER (PARTITION BY GroupID) AS maxUniqueDate
    FROM #Example
    ) calc_maxUD
WHERE maxUniqueDate > GroupDate
    AND maxUniqueDate = UniqueDate

DROP TABLE #Example

dbfiddle hier

John Eisbrener
quelle
2
Wenn Sie die Leistung einer Abfrage optimieren möchten, sind die Indizes in Ihrer Tabelle ein sehr wichtiger Teil der Frage.
Daniel Hutmacher
@DanielHutmacher Ich stimme vollkommen zu, obwohl ich kein Schema für meinen DWH- und Staging-Bereich ausgeben werde. Dies ist also das Beste, was ich im Rahmen der Vernunft tun kann.
John Eisbrener

Antworten:

9

Ich gehe davon aus, dass es keinen Index gibt, da Sie keinen angegeben haben.

Der folgende Index eliminiert auf Anhieb einen Sortieroperator in Ihrem Plan, der andernfalls möglicherweise viel Speicher verbrauchen würde:

CREATE INDEX IX ON #Example (GroupID, UniqueDate) INCLUDE (UniqueID, GroupDate);

Die Unterabfrage ist in diesem Fall kein Leistungsproblem. Wenn überhaupt, würde ich nach Möglichkeiten suchen, die Fensterfunktion (MAX ... OVER) zu entfernen, um das Konstrukt Nested Loop und Table Spool zu vermeiden.

Mit demselben Index sieht die folgende Abfrage auf den ersten Blick weniger effizient aus und führt zwar von zwei auf drei Scans in der Basistabelle, eliminiert jedoch eine große Anzahl interner Lesevorgänge, da Spool-Operatoren fehlen. Ich vermute, dass die Leistung immer noch besser ist, insbesondere wenn Sie über genügend CPU-Kerne und E / A-Leistung auf Ihrem Server verfügen:

SELECT e.UniqueID
     , e.GroupID
     , e.GroupDate
     , e.UniqueDate
FROM (
    SELECT GroupID, MAX(UniqueDate) AS maxUniqueDate
    FROM #Example
    GROUP BY GroupID) AS agg
INNER JOIN #Example AS e ON agg.GroupID=e.GroupID
WHERE agg.maxUniqueDate > e.GroupDate
    AND agg.maxUniqueDate = e.UniqueDate
OPTION (MERGE JOIN);

(Hinweis: Ich habe einen MERGE JOINAbfragehinweis hinzugefügt , dies sollte jedoch wahrscheinlich automatisch geschehen, wenn Ihre Statistiken in Ordnung sind. Es wird empfohlen, solche Hinweise wegzulassen, wenn Sie können.)

Daniel Hutmacher
quelle
6
Es ist hässlicher, aber der Ausführungsplan ist hübscher. Das ist die Magie deklarativer Sprachen wie T-SQL.
Daniel Hutmacher
11

Wann und wenn Sie ein Upgrade von SQL Server 2012 auf SQL Server 2016 durchführen können, können Sie möglicherweise die stark verbesserte Leistung (insbesondere für rahmenlose Fensteraggregate) nutzen, die der neue Fensteraggregatoperator im Stapelmodus bietet.

Fast alle großen Datenverarbeitungsszenarien funktionieren mit Columnstore-Speicher besser als mit Rowstore. Auch ohne für Ihre Basistabellen zum Spaltenspeicher zu wechseln, können Sie die Vorteile der neuen Ausführung im Operator- und Stapelmodus 2016 nutzen, indem Sie einen leeren, nicht gruppierten, durch den Spaltenspeicher gefilterten Index für eine der Basistabellen erstellen oder redundant eine äußere Verknüpfung mit einem von einem Spaltenspeicher organisierten Speicher herstellen Tabelle.

Mit der zweiten Option wird die Abfrage zu:

-- Just to get batch mode processing and the window aggregate operator
CREATE TABLE #Dummy (a integer NOT NULL, INDEX DummyCC CLUSTERED COLUMNSTORE);

-- Identify any UniqueDates that are greater than the GroupDate within their GroupID
SELECT
    calc_maxUD.UniqueID,
    calc_maxUD.GroupID,
    calc_maxUD.GroupDate,
    calc_maxUD.UniqueDate
FROM 
(
    SELECT
        E.UniqueID,
        E.GroupID,
        E.GroupDate,
        E.UniqueDate,
        maxUniqueDate = MAX(UniqueDate) OVER (
            PARTITION BY GroupID)
    FROM #Example AS E
    LEFT JOIN #Dummy AS D -- The only change to the original query
        ON 1 = 0
) AS calc_maxUD
WHERE 
    calc_maxUD.maxUniqueDate > calc_maxUD.GroupDate
    AND calc_maxUD.maxUniqueDate = calc_maxUD.UniqueDate;

db <> Geige

Beachten Sie, dass die einzige Änderung an der ursprünglichen Abfrage darin besteht, eine leere temporäre Tabelle zu erstellen und den linken Join hinzuzufügen. Der Ausführungsplan lautet:

Batch-Modus Fenster Aggregatplan

(58 row(s) affected)
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0
Table '#Example'. Scan count 1, logical reads 40, physical reads 0, read-ahead reads 0

Weitere Informationen und Optionen finden Sie in der hervorragenden Serie von Itzik Ben-Gan, Was Sie über den Aggregatoperator für Stapelfenster in SQL Server 2016 wissen müssen (in drei Teilen).

Paul White 9
quelle
7

Ich werde nur das alte Kreuz werfen.

SELECT e.*
    FROM #Example AS e
    CROSS APPLY ( SELECT TOP 1 e2.UniqueDate AS maxUniqueDate
                    FROM #Example AS e2
                    WHERE e2.GroupID = e.GroupID 
                    ORDER BY e2.UniqueDate DESC
                    ) AS ca
    WHERE ca.maxUniqueDate > e.GroupDate
        AND ca.maxUniqueDate = e.UniqueDate;

Mit einigen Indizes funktioniert es ziemlich gut.

CREATE CLUSTERED INDEX cx_whatever ON #Example (GroupID)

CREATE UNIQUE NONCLUSTERED INDEX ix_whatever ON #Example (GroupID, UniqueDate DESC, GroupDate)

Die Statistikzeit und io sehen so aus (Ihre Anfrage ist das erste Ergebnis)

Table 'Worktable'. Scan count 3, logical reads 28004, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table '#Example'. Scan count 1, logical reads 51, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 15 ms,  elapsed time = 20 ms.

Table '#Example'. Scan count 10001, logical reads 21336, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 16 ms,  elapsed time = 11 ms.

Abfragepläne sind hier (wieder ist Ihre zuerst):

https://www.brentozar.com/pastetheplan/?id=BJYJvqAal

Warum bevorzuge ich diese Version? Ich vermeide die Spulen. Wenn diese auf die Festplatte verschüttet werden, wird es hässlich.

Aber vielleicht möchten Sie das auch ausprobieren.

SELECT e.*
    FROM #Example AS e
    CROSS APPLY ( SELECT e2.UniqueDate AS maxUniqueDate
                    FROM #Example AS e2
                    WHERE e2.GroupID = e.GroupID 
                    ) AS ca
    WHERE ca.maxUniqueDate > e.GroupDate
        AND ca.maxUniqueDate = e.UniqueDate;

Wenn dies ein großer DW ist, bevorzugen Sie möglicherweise den Hash-Join und die Zeilenfilterung im Join, anstatt am Ende der TOP 1Abfrage als Filteroperator.

Plan ist hier: https://www.brentozar.com/pastetheplan/?id=BkUF55ATx

Statistik Zeit und io hier:

Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table '#Example'. Scan count 2, logical reads 84, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 16 ms,  elapsed time = 5 ms.

Hoffe das hilft!

Eine Bearbeitung, basierend auf der Idee von @ ypercube, und ein neuer Index.

CREATE NONCLUSTERED INDEX ix_meh ON #Example (UniqueDate,GroupDate) INCLUDE (UniqueID,GroupID);

WITH t1 AS 
(
    SELECT DISTINCT
    e.GroupID ,
    MAX(UniqueDate) AS MaxUniqueDate
    FROM #Example AS e
    GROUP BY e.GroupID
)
SELECT *
FROM #Example AS e
CROSS APPLY (
SELECT *
FROM t1
    WHERE t1.MaxUniqueDate > e.GroupDate
        AND t1.MaxUniqueDate = e.UniqueDate
        AND t1.GroupID = e.GroupID
) ca

Hier ist die Statistik Zeit und io:

Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table '#Example'. Scan count 2, logical reads 91, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 4 ms.

Hier ist der Plan:

https://www.brentozar.com/pastetheplan/?id=SJv8foR6g

Erik Darling
quelle
Mein Beispiel schien etwas zu sauber zu sein, da es Szenarien gibt, in denen ich in meiner tatsächlichen Umgebung mehrere eindeutige Daten haben kann, die größer als das Gruppendatum sind. Diese Bedingung macht Ihre 2nd Cross Apply-Abfrage ungültig, aber die anderen Ansätze funktionieren beide ohne Probleme. Vielen Dank für einige weitere Optionen!
John Eisbrener
4

Ich würde einen Blick darauf werfen top with ties

Wenn GroupDateist das gleiche pro GroupIddann:

select top 1 with ties 
   UniqueID
 , GroupID
 , GroupDate
 , UniqueDate
from #Example
where UniqueDate > GroupDate
order by row_number() over (partition by GroupId order by UniqueDate desc)

Sonst: Verwendung top with tiesin einem gemeinsamen Tabellenausdruck

with cte as (
  select top 1 with ties 
      UniqueID
    , GroupID
    , GroupDate
    , UniqueDate
  from #Example
  order by row_number() over (partition by GroupId order by UniqueDate desc)
)
select *
from cte
where UniqueDate > GroupDate

dbfiddle: http://dbfiddle.uk/?rdbms=sqlserver_2016&fiddle=c058994c2f5f3d99b212f06e1dae9fd3

Ursprüngliche Abfrage

Table 'Worktable'. Scan count 3, logical reads 28001, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table '#Example____________________________________________________________________________________________________________0000000000CB'. Scan count 1, logical reads 43, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 31 ms,  elapsed time = 31 ms.

vs top with tiesin einem gemeinsamen Tabellenausdruck

Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table '#Example____________________________________________________________________________________________________________0000000000CB'. Scan count 1, logical reads 43, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 16 ms,  elapsed time = 15 ms.
SqlZim
quelle
4

Daher habe ich einige Analysen zu den verschiedenen bisher veröffentlichten Ansätzen durchgeführt, und in meiner Umgebung sieht es so aus, als würde Daniels Ansatz bei den Ausführungszeiten konsequent gewinnen. Überraschenderweise (für mich) war der dritte CROSS APPLY-Ansatz von sp_BlitzErik nicht so weit zurück. Hier sind die Ergebnisse, wenn jemand interessiert ist, aber danke einer TON für alle alternativen Ansätze. Ich habe mehr aus den Antworten auf diese Frage gelernt als seit einiger Zeit!

Windowed Function - baseline metric

(10406 row(s) affected)
Table 'DateDim'. Scan count 9, logical reads 791, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TableFact'. Scan count 9, logical reads 140181, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 89815, logical reads 42553550, physical reads 0, read-ahead reads 84586, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table01Dim'. Scan count 9, logical reads 7688, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table02Dim'. Scan count 9, logical reads 7819, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 87753 ms,  elapsed time = 13031 ms.
Warning: Null value is eliminated by an aggregate or other SET operation.


Basic Aggregated Subquery - Daniel Hutmacher

(10406 row(s) affected)
Table 'DateDim'. Scan count 18, logical reads 1194, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TableFact'. Scan count 18, logical reads 280362, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 48, logical reads 82408, physical reads 9629, read-ahead reads 72779, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 89791, logical reads 6861425, physical reads 0, read-ahead reads 14565, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table01Dim'. Scan count 9, logical reads 7688, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table02Dim'. Scan count 18, logical reads 15726, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 40527 ms,  elapsed time = 6182 ms.
Warning: Null value is eliminated by an aggregate or other SET operation.


CROSS APPLY Operation A - sp_BlitzErik

(10406 row(s) affected)
Table 'DateDim'. Scan count 9, logical reads 6199331, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TableFact'. Scan count 3099273, logical reads 12844012, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table01Dim'. Scan count 3109676, logical reads 9350502, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table02Dim'. Scan count 3109676, logical reads 9482456, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 132632 ms,  elapsed time = 20955 ms.


CROSS APPLY Operation C - sp_BlitzErik

(10406 row(s) affected)
Table 'DateDim'. Scan count 18, logical reads 1194, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TableFact'. Scan count 18, logical reads 280362, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 56, logical reads 92800, physical reads 10872, read-ahead reads 81928, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 89791, logical reads 6861425, physical reads 0, read-ahead reads 14563, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table01Dim'. Scan count 18, logical reads 15376, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table02Dim'. Scan count 18, logical reads 15726, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 46082 ms,  elapsed time = 6804 ms.
Warning: Null value is eliminated by an aggregate or other SET operation.


TOP 1 WITH TIES - B - SqlZim

(10406 row(s) affected)
Table 'DateDim'. Scan count 9, logical reads 791, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TableFact'. Scan count 9, logical reads 140181, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 89791, logical reads 6866304, physical reads 0, read-ahead reads 93468, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table01Dim'. Scan count 9, logical reads 7688, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table02Dim'. Scan count 9, logical reads 7835, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 79406 ms,  elapsed time = 15852 ms.
John Eisbrener
quelle
Ich habe mir nur angesehen, wie sich die veröffentlichten Optionen stapeln würden, wenn ich Ihr Beispiel auf 100.000 Zeilen erhöhen und alle Indexvorschläge aller hinzufügen würde. Scheint auch ziemlich repräsentativ für Ihre tatsächlichen Ergebnisse zu sein. Es scheint meine Version von top with tiesSchnallen mit so vielen Reihen. dbfiddle.uk/…
SqlZim