Wie kann ich die Gesamtzahl der letzten Zeilen schneller ausführen?

8

Ich entwerfe gerade eine Transaktionstabelle. Ich erkannte, dass die Berechnung der laufenden Summen für jede Zeile erforderlich ist und die Leistung möglicherweise langsam ist. Also habe ich zu Testzwecken eine Tabelle mit 1 Million Zeilen erstellt.

CREATE TABLE [dbo].[Table_1](
    [seq] [int] IDENTITY(1,1) NOT NULL,
    [value] [bigint] NOT NULL,
 CONSTRAINT [PK_Table_1] PRIMARY KEY CLUSTERED 
(
    [seq] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

Und ich habe versucht, 10 letzte Zeilen und ihre laufenden Summen zu erhalten, aber es hat ungefähr 10 Sekunden gedauert.

--1st attempt
SELECT TOP 10 seq
    ,value
    ,sum(value) OVER (ORDER BY seq) total
FROM Table_1
ORDER BY seq DESC

--(10 rows affected)
--Table 'Worktable'. Scan count 1000001, logical reads 8461526, physical reads 2, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
--Table 'Table_1'. Scan count 1, logical reads 2608, physical reads 516, read-ahead reads 2617, 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.
--
--(1 row affected)
--
-- SQL Server Execution Times:
--   CPU time = 8483 ms,  elapsed time = 9786 ms.

Ausführungsplan für den ersten Versuch

Ich habe TOPaufgrund der langsamen Leistung des Plans vermutet , dass ich die Abfrage wie folgt geändert habe, und es dauerte ungefähr 1 bis 2 Sekunden. Aber ich denke, das ist immer noch langsam für die Produktion und frage mich, ob dies weiter verbessert werden kann.

--2nd attempt
SELECT *
    ,(
        SELECT SUM(value)
        FROM Table_1
        WHERE seq <= t.seq
        ) total
FROM (
    SELECT TOP 10 seq
        ,value
    FROM Table_1
    ORDER BY seq DESC
    ) t
ORDER BY seq DESC

--(10 rows affected)
--Table 'Table_1'. Scan count 11, logical reads 26083, physical reads 1, read-ahead reads 443, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
--
--(1 row affected)
--
-- SQL Server Execution Times:
--   CPU time = 1422 ms,  elapsed time = 1621 ms.

Ausführungsplan für den 2. Versuch

Meine Fragen sind:

  • Warum ist die Abfrage vom ersten Versuch langsamer als die zweite?
  • Wie kann ich die Leistung weiter verbessern? Ich kann auch Schemata ändern.

Um klar zu sein, liefern beide Abfragen das gleiche Ergebnis wie unten.

Ergebnisse

user2652379
quelle
1
Normalerweise benutze ich keine Fensterfunktionen, aber ich erinnere mich, dass ich einige nützliche Artikel darüber gelesen habe. Schauen Sie sich eine Einführung in T-SQL-Fensterfunktionen an , insbesondere den Teil Fensteraggregatverbesserungen im Jahr 2012 . Vielleicht gibt es Ihnen einige Antworten. ... und ein weiterer Artikel des gleichen hervorragenden Autors T-SQL Window Funktionen und Leistung
Denis Rubashkin
Haben Sie versucht, einen Index zu erstellen value?
Jacob H

Antworten:

5

Ich empfehle, mit etwas mehr Daten zu testen, um eine bessere Vorstellung davon zu bekommen, was los ist, und um zu sehen, wie unterschiedliche Ansätze funktionieren. Ich habe 16 Millionen Zeilen in eine Tabelle mit derselben Struktur geladen. Den Code zum Auffüllen der Tabelle finden Sie am Ende dieser Antwort.

Der folgende Ansatz dauert auf meinem Computer 19 Sekunden:

SELECT TOP (10) seq
    ,value
    ,sum(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING) total
FROM dbo.[Table_1_BIG]
ORDER BY seq DESC;

Aktueller Plan hier . Die meiste Zeit wird damit verbracht, die Summe zu berechnen und die Sortierung durchzuführen. Besorgniserregend ist, dass der Abfrageplan fast die gesamte Arbeit für die gesamte Ergebnismenge erledigt und nach den 10 Zeilen filtert, die Sie ganz am Ende angefordert haben. Die Laufzeit dieser Abfrage skaliert mit der Größe der Tabelle anstatt mit der Größe der Ergebnismenge.

Diese Option dauert auf meinem Computer 23 Sekunden:

SELECT *
    ,(
        SELECT SUM(value)
        FROM dbo.[Table_1_BIG]
        WHERE seq <= t.seq
        ) total
FROM (
    SELECT TOP (10) seq
        ,value
    FROM dbo.[Table_1_BIG]
    ORDER BY seq DESC
    ) t
ORDER BY seq DESC;

Aktueller Plan hier . Dieser Ansatz skaliert sowohl mit der Anzahl der angeforderten Zeilen als auch mit der Größe der Tabelle. Fast 160 Millionen Zeilen werden aus der Tabelle gelesen:

Hallo

Um korrekte Ergebnisse zu erhalten, müssen Sie die Zeilen für die gesamte Tabelle summieren. Idealerweise würden Sie diese Summierung nur einmal durchführen. Dies ist möglich, wenn Sie die Herangehensweise an das Problem ändern. Sie können die Summe für die gesamte Tabelle berechnen und dann eine laufende Summe von den Zeilen in der Ergebnismenge abziehen. So können Sie die Summe für die N-te Zeile finden. Ein Weg, dies zu tun:

SELECT TOP (10) seq
,value
, [value]
    - SUM([value]) OVER (ORDER BY seq DESC ROWS UNBOUNDED PRECEDING)
    + (SELECT SUM([value]) FROM dbo.[Table_1_BIG]) AS total
FROM dbo.[Table_1_BIG]
ORDER BY seq DESC;

Aktueller Plan hier . Die neue Abfrage wird in 644 ms auf meinem Computer ausgeführt. Die Tabelle wird einmal gescannt, um die vollständige Summe zu erhalten. Anschließend wird für jede Zeile in der Ergebnismenge eine zusätzliche Zeile gelesen. Es gibt keine Sortierung und fast die gesamte Zeit wird für die Berechnung der Summe im parallelen Teil des Plans aufgewendet:

ziemlich gut

Wenn Sie möchten, dass diese Abfrage noch schneller ausgeführt wird, müssen Sie nur den Teil optimieren, der die gesamte Summe berechnet. Die obige Abfrage führt einen Clustered-Index-Scan durch. Der Clustered-Index enthält alle Spalten, Sie benötigen jedoch nur die [value]Spalte. Eine Möglichkeit besteht darin, einen nicht gruppierten Index für diese Spalte zu erstellen. Eine andere Option besteht darin, einen nicht gruppierten Spaltenspeicherindex für diese Spalte zu erstellen. Beides verbessert die Leistung. Wenn Sie in Enterprise arbeiten, können Sie eine indizierte Ansicht wie die folgende erstellen:

CREATE OR ALTER VIEW dbo.Table_1_BIG__SUM
WITH SCHEMABINDING
AS
SELECT SUM([value]) SUM_VALUE
, COUNT_BIG(*) FOR_U
FROM dbo.[Table_1_BIG];

GO

CREATE UNIQUE CLUSTERED INDEX CI ON dbo.Table_1_BIG__SUM (SUM_VALUE);

Diese Ansicht gibt eine einzelne Zeile zurück, sodass fast kein Speicherplatz belegt wird. Bei DML wird eine Strafe verhängt, die sich jedoch nicht wesentlich von der Indexpflege unterscheiden sollte. Mit der indizierten Ansicht im Spiel dauert die Abfrage jetzt 0 ms:

Geben Sie hier die Bildbeschreibung ein

Aktueller Plan hier . Das Beste an diesem Ansatz ist, dass die Laufzeit nicht durch die Größe der Tabelle geändert wird. Entscheidend ist nur, wie viele Zeilen zurückgegeben werden. Wenn Sie beispielsweise die ersten 10000 Zeilen erhalten, dauert die Ausführung der Abfrage jetzt 18 ms.

Code zum Auffüllen der Tabelle:

DROP TABLE IF EXISTS dbo.[Table_1_BIG];

CREATE TABLE dbo.[Table_1_BIG] (
    [seq] [int] NOT NULL,
    [value] [bigint] NOT NULL
);

DROP TABLE IF EXISTS #t;
CREATE TABLE #t (ID BIGINT);

INSERT INTO #t WITH (TABLOCK)
SELECT TOP (4000) -1 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

INSERT INTO dbo.[Table_1_BIG] WITH (TABLOCK)
SELECT t1.ID * 4000 + t2.ID, 8 * t2.ID + t1.ID
FROM (SELECT TOP (4000) ID FROM #t) t1
CROSS JOIN #t t2;

ALTER TABLE dbo.[Table_1_BIG]
ADD CONSTRAINT [PK_Table_1] PRIMARY KEY ([seq]);
Joe Obbish
quelle
4

Unterschied in den ersten beiden Ansätzen

Der erste Plan verbringt ungefähr 7 der 10 Sekunden im Window Spool-Operator. Dies ist der Hauptgrund, warum er so langsam ist. Es wird eine Menge E / A in Tempdb ausgeführt, um dies zu erstellen. Meine Statistiken I / O und Zeit sehen folgendermaßen aus:

Table 'Worktable'. Scan count 1000001, logical reads 8461526
Table 'Table_1'. Scan count 1, logical reads 2609
Table 'Worktable'. Scan count 0, logical reads 0

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

Der zweite Plan kann die Spule und damit den Arbeitstisch vollständig umgehen. Es werden einfach die obersten 10 Zeilen aus dem Clustered-Index abgerufen und anschließend werden verschachtelte Schleifen mit der Aggregation (Summe) verknüpft, die aus einem separaten Clustered-Index-Scan hervorgeht. Die Innenseite liest immer noch die gesamte Tabelle, aber die Tabelle ist sehr dicht, so dass dies mit einer Million Zeilen einigermaßen effizient ist.

Table 'Table_1'. Scan count 11, logical reads 26093
 SQL Server Execution Times:
   CPU time = 1563 ms,  elapsed time = 1671 ms.

Verbessernde Leistung

Columnstore

Wenn Sie wirklich den Ansatz der "Online-Berichterstellung" möchten, ist der Spaltenspeicher wahrscheinlich die beste Option.

ALTER TABLE [dbo].[Table_1] DROP CONSTRAINT [PK_Table_1];

CREATE CLUSTERED COLUMNSTORE INDEX [PK_Table_1] ON dbo.Table_1;

Dann ist diese Abfrage lächerlich schnell:

SELECT TOP 10
    seq, 
    value, 
    SUM(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING)
FROM dbo.Table_1
ORDER BY seq DESC;

Hier sind die Statistiken von meiner Maschine:

Table 'Table_1'. Scan count 4, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 3319
Table 'Table_1'. Segment reads 1, segment skipped 0.
Table 'Worktable'. Scan count 0, logical reads 0

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

Du wirst das wahrscheinlich nicht übertreffen (es sei denn, du bist wirklich schlau - nett, Joe). Columnstore ist verrückt gut darin, große Datenmengen zu scannen und zu aggregieren.

Verwenden Sie ROWanstelle der RANGEFensterfunktionsoption

Mit diesem Ansatz, der in einer anderen Antwort erwähnt wurde und den ich im obigen Beispiel für den Spaltenspeicher verwendet habe ( Ausführungsplan ), können Sie eine sehr ähnliche Leistung wie bei Ihrer zweiten Abfrage erzielen :

SELECT TOP 10
    seq, 
    value, 
    SUM(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING)
FROM dbo.Table_1
ORDER BY seq DESC;

Dies führt zu weniger Lesevorgängen als bei Ihrem zweiten Ansatz und zu keiner Tempdb-Aktivität im Vergleich zu Ihrem ersten Ansatz, da die Fensterspool im Speicher auftritt :

... RANGE verwendet einen On-Disk-Spool, während ROWS einen In-Memory-Spool verwendet

Leider entspricht die Laufzeit in etwa Ihrer zweiten Vorgehensweise.

Table 'Worktable'. Scan count 0, logical reads 0
Table 'Table_1'. Scan count 1, logical reads 2609
Table 'Worktable'. Scan count 0, logical reads 0

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

Schemabasierte Lösung: Asynchrone laufende Summen

Da Sie offen für andere Ideen sind, können Sie die "laufende Summe" asynchron aktualisieren. Sie können die Ergebnisse einer dieser Abfragen regelmäßig in eine Tabelle "Summen" laden. Sie würden also so etwas tun:

CREATE TABLE [dbo].[Table_1_Totals]
(
    [seq] [int] NOT NULL,
    [running_total] [bigint] NOT NULL,
    CONSTRAINT [PK_Table_1_Totals] PRIMARY KEY CLUSTERED ([seq])
);

Laden Sie es jeden Tag / jede Stunde / was auch immer (dies dauerte auf meiner Maschine mit 1-mm-Reihen ungefähr 2 Sekunden und konnte optimiert werden):

INSERT INTO dbo.Table_1_Totals
SELECT
    seq, 
    SUM(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING) as total
FROM dbo.Table_1 t
WHERE NOT EXISTS (
            SELECT NULL 
            FROM dbo.Table_1_Totals t2
            WHERE t.seq = t2.seq)
ORDER BY seq DESC;

Dann ist Ihre Berichtsabfrage sehr effizient:

SELECT TOP 10
    t.seq, 
    t.value, 
    t2.running_total
FROM dbo.Table_1 t
    INNER JOIN dbo.Table_1_Totals t2
        ON t.seq = t2.seq
ORDER BY seq DESC;

Hier sind die gelesenen Statistiken:

Table 'Table_1'. Scan count 0, logical reads 35
Table 'Table_1_Totals'. Scan count 1, logical reads 3

Schemabasierte Lösung: In-Row-Summen mit Einschränkungen

Eine wirklich interessante Lösung hierfür wird in dieser Antwort auf die Frage ausführlich beschrieben: Schreiben eines einfachen Bankschemas: Wie soll ich meine Salden mit dem Transaktionsverlauf synchronisieren?

Der grundlegende Ansatz wäre, die aktuelle laufende Summe in Reihe zusammen mit der vorherigen laufenden Summe und der Sequenznummer zu verfolgen. Anschließend können Sie mithilfe von Einschränkungen überprüfen, ob die laufenden Summen immer korrekt und aktuell sind.

Dank an Paul White für die Bereitstellung einer Beispielimplementierung für das Schema in dieser Frage und Antwort:

CREATE TABLE dbo.Table_1
(
    seq integer IDENTITY(1,1) NOT NULL,
    val bigint NOT NULL,
    total bigint NOT NULL,

    prev_seq integer NULL,
    prev_total bigint NULL,

    CONSTRAINT [PK_Table_1] 
        PRIMARY KEY CLUSTERED (seq ASC),

    CONSTRAINT [UQ dbo.Table_1 seq, total]
        UNIQUE (seq, total),

    CONSTRAINT [UQ dbo.Table_1 prev_seq]
        UNIQUE (prev_seq),

    CONSTRAINT [FK dbo.Table_1 previous seq and total]
        FOREIGN KEY (prev_seq, prev_total) 
        REFERENCES dbo.Table_1 (seq, total),

    CONSTRAINT [CK dbo.Table_1 total = prev_total + val]
        CHECK (total = ISNULL(prev_total, 0) + val),

    CONSTRAINT [CK dbo.Table_1 denormalized columns all null or all not null]
        CHECK 
        (
            (prev_seq IS NOT NULL AND prev_total IS NOT NULL)
            OR
            (prev_seq IS NULL AND prev_total IS NULL)
        )
);
Josh Darnell
quelle
2

Bei einer so kleinen Teilmenge der zurückgegebenen Zeilen ist die dreieckige Verknüpfung eine gute Option. Wenn Sie jedoch Fensterfunktionen verwenden, stehen Ihnen mehr Optionen zur Verfügung, mit denen Sie die Leistung steigern können. Die Standardoption für die Fensteroption ist BEREICH, die optimale Option ist jedoch REIHEN. Beachten Sie, dass der Unterschied nicht nur in der Leistung liegt, sondern auch in den Ergebnissen, wenn es um Bindungen geht.

Der folgende Code ist etwas schneller als die von Ihnen vorgestellten.

SELECT TOP 10 seq
    ,value
    ,sum(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING) total
FROM Table_1
ORDER BY seq DESC
Luis Cazares
quelle
Danke, dass du es erzählt hast ROWS. Ich habe es versucht, aber ich kann nicht sagen, dass es schneller ist als meine zweite Abfrage. Das Ergebnis warCPU time = 1438 ms, elapsed time = 1537 ms.
user2652379
Dies gilt jedoch nur für diese Option. Ihre zweite Abfrage lässt sich nicht gut skalieren. Versuchen Sie, mehr Zeilen zurückzugeben, und der Unterschied wird ziemlich offensichtlich.
Luis Cazares
Vielleicht außerhalb von t-sql? Ich kann das Schema ändern.
user2652379