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.
Ich habe TOP
aufgrund 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.
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.
sql-server
database-design
t-sql
query-performance
execution-plan
user2652379
quelle
quelle
value
?Antworten:
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:
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:
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:
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:
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:
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: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:
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:
quelle
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:
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.
Verbessernde Leistung
Columnstore
Wenn Sie wirklich den Ansatz der "Online-Berichterstellung" möchten, ist der Spaltenspeicher wahrscheinlich die beste Option.
Dann ist diese Abfrage lächerlich schnell:
Hier sind die Statistiken von meiner Maschine:
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
ROW
anstelle derRANGE
FensterfunktionsoptionMit 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 :
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 :
Leider entspricht die Laufzeit in etwa Ihrer zweiten Vorgehensweise.
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:
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):
Dann ist Ihre Berichtsabfrage sehr effizient:
Hier sind die gelesenen Statistiken:
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:
quelle
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.
quelle
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.