Partitionierung durch aufsteigende und absteigende Leistung

7

Ich habe eine Tabelle, und für einen bestimmten Satz von Feldern a, b und c muss ich die erste und letzte Zeile nach d und e sortieren und verwende ROW_NUMBER, um diese Zeilen abzurufen. Der relevante Teil der Erklärung ist ...

ROW_NUMBER() OVER (PARTITION BY a,b,c ORDER BY d ASC, e ASC) AS row_number_start,
ROW_NUMBER() OVER (PARTITION BY a,b,c ORDER BY d DESC, e DESC) AS row_number_end

Der Ausführungsplan zeigt zwei Sortiervorgänge, jeweils einen für jeden. Diese Sortiervorgänge machen über 60% der Gesamtkosten der Anweisung aus (wir sprechen hier von zig Millionen Zeilen, die Partitionen haben normalerweise 1-100 Datensätze pro Partition, meistens unter 10).

Es wäre also gut, wenn ich einen von ihnen loswerden könnte. Ich habe versucht, einen Index zu erstellen, um die Sortierung zu replizieren. Dies eliminierte eine der Sortieroperationen, aber nicht die letztere. (Beachten Sie, dass jeder erstellte Index nur für diesen Prozess von Nutzen ist und im Rahmen eines ETL-Prozesses täglich neu erstellt wird.)

Nach Prüfung des Ausführungsplans besteht das Problem meines Erachtens darin, dass SQL Server bei der Ausführung einer Partition nach Anweisung darauf besteht, die Reihenfolge der Partitionierungsspalten aufsteigend zu ändern. Logischerweise spielt es keine Rolle, ob Sie aufsteigend oder absteigend sortieren, und wenn der Optimierer dies verstanden hat, kann er denselben Index einfach rückwärts lesen, um row_number_end zu berechnen.

Gibt es eine Möglichkeit, den Optimierer hier sinnvoll zu machen, oder kann jemand einen alternativen Ansatz vorschlagen, um das gleiche Endziel zu erreichen?

WelshGandalf
quelle

Antworten:

8

Beispieltabelle und Index

CREATE TABLE dbo.Test
(
    a integer NOT NULL,
    b integer NOT NULL,
    c integer NOT NULL,
    d integer NOT NULL,
    e integer NOT NULL
);

CREATE INDEX i1 ON dbo.Test (a, b, c, d, e);

1. Lösung auftragen

SELECT
    DT.a,
    DT.b,
    DT.c,
    FL.d,
    FL.e 
FROM 
(
    -- Could be an indexed view
    SELECT
        T.a,
        T.b,
        T.c
    FROM dbo.Test AS T
    GROUP BY
        T.a,
        T.b,
        T.c
) AS DT
CROSS APPLY 
(
    (
        -- First
        SELECT TOP (1)
            T2.d,
            T2.e
        FROM  dbo.Test AS T2
        WHERE
            T2.a = DT.a
            AND T2.b = DT.b
            AND T2.c = DT.c
        ORDER BY
            T2.d ASC,
            T2.e ASC
    )

    UNION ALL

    (
        -- Last
        SELECT TOP (1)
            T3.d,
            T3.e
        FROM  dbo.Test AS T3
        WHERE
            T3.a = DT.a
            AND T3.b = DT.b
            AND T3.c = DT.c
        ORDER BY
            T3.d DESC,
            T3.e DESC
    )
) AS FL;

Ausführungsplan:

Planen

2. Zeilennummerierungslösung

Eine Alternative, möglicherweise besser, wenn es im Durchschnitt eine kleine Anzahl von Zeilen pro Gruppe gibt: (verbessert auf Vorschlag von Martin Smith)

Abfrage

SELECT
    TF.a,
    TF.b,
    TF.c,
    TF.d,
    TF.e
FROM
(
    SELECT
        T.*,
        rn = ROW_NUMBER() OVER (
                PARTITION BY a,b,c 
                ORDER BY d ASC, e ASC)
    FROM dbo.Test AS T
) AS TF
WHERE
    TF.rn = 1

UNION ALL

SELECT
    TL2.a,
    TL2.b,
    TL2.c,
    TL2.d,
    TL2.e
FROM 
(
    -- TOP (max bigint) to allow an ORDER BY in this scope
    SELECT TOP (9223372036854775807)
        TL.a,
        TL.b,
        TL.c,
        TL.d,
        TL.e
    FROM 
    (
        SELECT
            T.*,
            rn = ROW_NUMBER() OVER (
                    PARTITION BY a,b,c 
                    ORDER BY d DESC, e DESC)
        FROM dbo.Test AS T
    ) AS TL
    WHERE
        TL.rn = 1
    ORDER BY
        -- To allow the optimizer to scan the index backward
        -- (Workaround for PARTITION BY being assumed ASC)
        TL.a DESC,
        TL.b DESC,
        TL.c DESC,
        TL.d DESC,
        TL.e DESC
) AS TL2;

Ausführungsplan:

Plan 2

3. LEAD verwenden

Dies basiert auf der Idee, dass die erste Zeile die Zeilennummer 1 und die letzte Zeile die Zeile vor der Zeilennummer 1 ist:

SELECT
    L.a,
    L.b,
    L.c,
    L.d,
    L.e
FROM 
(
    -- Add LEAD(1) on numbering
    SELECT 
        N.*,
        next_rn = LEAD(N.rn, 1, 1) OVER (
            PARTITION BY N.a, N.b, N.c
            ORDER BY N.d, N.e) 
    FROM 
    (
        -- Numbered
        SELECT
            T.*,
            rn = ROW_NUMBER() OVER (
                PARTITION BY T.a, T.b, T.c
                ORDER BY T.d, T.e) 
        FROM dbo.Test AS T
    ) AS N
) AS L
WHERE
    -- This row is first, or the next one is
    L.rn = 1
    OR L.next_rn = 1;

Ausführungsplan

Plan 3


Um die Kommentardiskussion zusammenzufassen:

  • Wenn Sie zusätzliche Spalten benötigen, fügen Sie diese einfach an den entsprechenden Stellen zu den Abfragen hinzu und stellen Sie sicher, dass sie im Index enthalten sind.
  • Nach der Erstellung können Abfragen möglicherweise viele Male von Indizes profitieren. Bei einer expliziten Sortierung im Ausführungsplan erfolgt die Sortierung bei jeder Ausführung . Außerdem können Indexerstellungssortierungen bei Bedarf dynamisch mehr Speicher erfassen. Dies ist bei regulären Sortierungen nicht der Fall - sie erhalten eine feste Zuordnung basierend auf Optimierungsschätzungen, und das war's. Wenn Sie die Zuordnung überschreiten, wird die Sortierung auf die Festplatte übertragen.
  • Der Apply-Ansatz ist für relativ große Gruppen optimal. Die Schätzung beträgt 2 Zeilen pro Iteration, aber der 'tatsächliche' Wert gilt für alle Iterationen (SSMS- Entwurfsentscheidung ). Eine sehr große Anzahl von Iterationen ist schlecht für die Anwendung.
Paul White 9
quelle