it-swarm.com.de

Unterabfrage mit Fensterfunktion optimieren

Da sich meine Fähigkeiten zur Leistungsoptimierung nie ausreichend anfühlen, frage ich mich immer, ob es mehr Optimierung gibt, die ich gegen einige Abfragen durchführen kann. Die Situation, auf die sich diese Frage bezieht, ist eine Windows 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 das Design 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 Datensätze zurück, nach denen ich suche. 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()[email protected]+5
    END
    ELSE
    BEGIN
        SET @UniqueDate = GETDATE()[email protected]
    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

8
John Eisbrener

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 es werden zwar zwei bis drei Scans in der Basistabelle durchgeführt, es wird jedoch eine große Anzahl interner Lesevorgänge eliminiert, da keine Spool-Operatoren vorhanden sind. Ich vermute, dass es immer noch besser funktioniert, insbesondere wenn Sie über genügend CPU-Kerne und IO Leistung auf Ihrem Server:

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 JOIN - Abfragehinweis hinzugefügt, der jedoch wahrscheinlich automatisch erfolgen sollte, wenn Ihre Statistiken in Ordnung sind. Es wird empfohlen, Hinweise wie diese wegzulassen, wenn Sie können.)

9

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 zum Spaltenspeicher für Ihre Basistabellen zu wechseln, können Sie die Vorteile der neuen Ausführung im Operator- und Stapelmodus 2016 nutzen, indem Sie einen leeren, nicht gruppierten, durch einen Spaltenspeicher gefilterten Index für eine der Basistabellen erstellen oder redundant eine äußere Verknüpfung mit einem von einem Spaltenspeicher organisierten Index 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 <> fiddle

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 mode window aggregate plan

(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).

11
Paul White 9

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 ist 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 anstelle des Endes im TOP 1 Abfrage 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

7
Erik Darling

Ich würde mir top with ties

Wenn GroupDate pro GroupId gleich ist, dann:

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: mit top with ties in einem allgemeiner 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=c058994c2f5f3d99b212f06e1dae9fd

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 ties in einem allgemeiner 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.
4
SqlZim

Daher habe ich einige Analysen zu den verschiedenen bisher veröffentlichten Ansätzen durchgeführt, und in meiner Umgebung sieht es so aus, als ob Daniels Ansatz bei den Ausführungszeiten beständig gewinnt. Überraschenderweise war sp_BlitzEriks dritter CROSS APPLY-Ansatz (für mich) 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.
4
John Eisbrener