Database
 sql >> Datenbank >  >> RDS >> Database

T-SQL-Bugs, Fallstricke und Best Practices – Fensterfunktionen

Dieser Artikel ist der vierte Teil einer Reihe über T-SQL-Bugs, Fallstricke und Best Practices. Zuvor habe ich Determinismus, Unterabfragen und Verknüpfungen behandelt. Der Schwerpunkt des Artikels dieses Monats liegt auf Fehlern, Fallstricken und Best Practices im Zusammenhang mit Fensterfunktionen. Vielen Dank an Erland Sommarskog, Aaron Bertrand, Alejandro Mesa, Umachandar Jayachandran (UC), Fabiano Neves Amorim, Milos Radivojevic, Simon Sabin, Adam Machanic, Thomas Grohser, Chan Ming Man und Paul White für Ihre Ideen!

In meinen Beispielen verwende ich eine Beispieldatenbank namens TSQLV5. Das Skript, das diese Datenbank erstellt und füllt, finden Sie hier und ihr ER-Diagramm hier.

Es gibt zwei häufige Fallstricke bei Fensterfunktionen, die beide das Ergebnis kontraintuitiver impliziter Standardeinstellungen sind, die vom SQL-Standard auferlegt werden. Eine Falle hat mit Berechnungen von laufenden Summen zu tun, bei denen Sie einen Fensterrahmen mit der impliziten Option RANGE erhalten. Ein weiterer Fallstrick ist etwas verwandt, hat aber schwerwiegendere Konsequenzen, da er eine implizite Frame-Definition für die Funktionen FIRST_VALUE und LAST_VALUE beinhaltet.

Fensterrahmen mit impliziter RANGE-Option

Unsere erste Falle besteht in der Berechnung laufender Summen mithilfe einer aggregierten Fensterfunktion, bei der Sie die Fensterreihenfolgeklausel explizit angeben, aber nicht explizit die Fensterrahmeneinheit (ROWS oder RANGE) und die zugehörige Fensterrahmenausdehnung, z. B. ROWS UNBEGRENZTES VORHERIGES. Der implizite Ausfall ist kontraintuitiv und seine Folgen könnten überraschend und schmerzhaft sein.

Um diese Falle zu demonstrieren, verwende ich eine Tabelle mit dem Namen Transaktionen, die zwei Millionen Bankkontotransaktionen mit Guthaben (positive Werte) und Lastschriften (negative Werte) enthält. Führen Sie den folgenden Code aus, um die Transaktionstabelle zu erstellen und mit Beispieldaten zu füllen:

 SETZE ZÄHLER EIN; TSQLV5 VERWENDEN; -- http://tsql.solidq.com/SampleDatabases/TSQLV5.zip DROP TABLE IF EXISTS dbo.Transactions; CREATE TABLE dbo.Transactions (actid INT NOT NULL, tranid INT NOT NULL, val MONEY NOT NULL, CONSTRAINT PK_Transactions PRIMARY KEY(actid, tranid) – erstellt POC-Index); DECLARE @num_partitions AS INT =100, @rows_per_partition AS INT =20000; INSERT INTO dbo.Transactions WITH (TABLOCK) (actid, tranid, val) SELECT NP.n, RPP.n, (ABS(CHECKSUM(NEWID())%2)*2-1) * (1 + ABS(CHECKSUM( NEWID())%5)) FROM dbo.GetNums(1, @num_partitions) AS NP CROSS JOIN dbo.GetNums(1, @rows_per_partition) AS RPP;

Unsere Falle hat sowohl eine logische Seite mit einem potenziellen logischen Fehler als auch eine Leistungsseite mit einer Leistungseinbuße. Die Leistungseinbuße ist nur relevant, wenn die Fensterfunktion mit Zeilenmodus-Verarbeitungsoperatoren optimiert wird. SQL Server 2016 führt den Window Aggregate-Operator im Batchmodus ein, der den Leistungseinbußenteil des Fallstricks beseitigt, aber vor SQL Server 2019 wird dieser Operator nur verwendet, wenn Sie einen Columnstore-Index für die Daten vorhanden haben. SQL Server 2019 führt den Batch-Modus für die Rowstore-Unterstützung ein, sodass Sie eine Batch-Modus-Verarbeitung erhalten können, selbst wenn keine Columnstore-Indizes für die Daten vorhanden sind. Wenn Sie die Codebeispiele in diesem Artikel auf SQL Server 2019 oder höher oder auf Azure SQL-Datenbank ausführen, verwenden Sie den folgenden Code, um den Datenbank-Kompatibilitätsgrad auf 140 festzulegen, um die Leistungseinbußen bei der Verarbeitung im Zeilenmodus zu demonstrieren um den Batch-Modus noch nicht für den Zeilenspeicher zu aktivieren:

 ALTER DATABASE TSQLV5 SET COMPATIBILITY_LEVEL =140;

Verwenden Sie den folgenden Code, um Zeit- und E/A-Statistiken in der Sitzung zu aktivieren:

 SET STATISTICS TIME, IO ON;

Um zu vermeiden, dass zwei Millionen Zeilen in SSMS gedruckt werden, schlage ich vor, die Codebeispiele in diesem Abschnitt mit aktivierter Option Ergebnisse nach Ausführung verwerfen auszuführen (gehen Sie zu Abfrageoptionen, Ergebnisse, Raster und aktivieren Sie Ergebnisse nach Ausführung verwerfen).

Bevor wir zum Fallstrick kommen, betrachten Sie die folgende Abfrage (nennen Sie sie Abfrage 1), die den Bankkontostand nach jeder Transaktion berechnet, indem sie eine laufende Summe unter Verwendung einer Fensteraggregatfunktion mit einer expliziten Frame-Spezifikation anwendet:

 SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ROWS UNBOUNDED PRECEDING ) AS balance FROM dbo.Transactions;

Der Plan für diese Abfrage mit Verarbeitung im Zeilenmodus ist in Abbildung 1 dargestellt.

Abbildung 1:Plan für Abfrage 1, Verarbeitung im Zeilenmodus

Der Plan ruft die vorgeordneten Daten aus dem gruppierten Index der Tabelle ab. Dann verwendet es die Segment- und Sequence-Projektoperatoren, um Zeilennummern zu berechnen, um herauszufinden, welche Zeilen zum Frame der aktuellen Zeile gehören. Dann verwendet es die Operatoren Segment, Window Spool und Stream Aggregate, um die Fensteraggregatfunktion zu berechnen. Der Window Spool-Operator wird verwendet, um die Rahmenzeilen zu spoolen, die dann aggregiert werden müssen. Ohne eine spezielle Optimierung hätte der Plan pro Zeile alle zutreffenden Rahmenzeilen in die Spule schreiben und sie dann aggregieren müssen. Dies hätte zu einer quadratischen oder N-Komplexität geführt. Die gute Nachricht ist, dass, wenn der Frame mit UNBOUNDED PRECEDING beginnt, SQL Server den Fall als fast track identifiziert Fall, in dem einfach die laufende Summe der vorherigen Zeile genommen und der Wert der aktuellen Zeile addiert wird, um die laufende Summe der aktuellen Zeile zu berechnen, was zu einer linearen Skalierung führt. In diesem Fast-Track-Modus schreibt der Plan nur zwei Zeilen pro Eingabezeile in die Spule – eine mit dem Aggregat und eine mit dem Detail.

Die Fensterspule kann auf zwei Arten physisch implementiert werden. Entweder als schneller In-Memory-Spool, der speziell für Fensterfunktionen entwickelt wurde, oder als langsamer On-Disk-Spool, der im Wesentlichen eine temporäre Tabelle in tempdb ist. Wenn die Anzahl der Zeilen, die pro zugrundeliegender Zeile in die Spule geschrieben werden müssen 10.000 überschreiten könnte, oder wenn SQL Server die Zahl nicht vorhersagen kann, wird der langsamere Spool auf dem Datenträger verwendet. In unserem Abfrageplan werden pro zugrundeliegender Zeile genau zwei Zeilen in die Spule geschrieben, sodass SQL Server die speicherinterne Spule verwendet. Leider ist aus dem Plan nicht ersichtlich, welche Art von Spule Sie bekommen. Es gibt zwei Möglichkeiten, dies herauszufinden. Eine besteht darin, ein erweitertes Ereignis namens window_spool_ondisk_warning zu verwenden. Eine weitere Option besteht darin, STATISTICS IO zu aktivieren und die Anzahl der logischen Lesevorgänge zu überprüfen, die für eine Tabelle namens Worktable gemeldet werden. Eine Zahl größer als Null bedeutet, dass Sie den Spool auf der Festplatte erhalten haben. Null bedeutet, dass Sie die In-Memory-Spule erhalten haben. Hier ist die E/A-Statistik für unsere Abfrage:

Logische Lesevorgänge der Tabelle „Arbeitstabelle“:0. Logische Lesevorgänge der Tabelle „Transaktionen“:6208.

Wie Sie sehen können, haben wir die In-Memory-Spule verwendet. Das ist im Allgemeinen der Fall, wenn Sie die ROWS-Fensterrahmeneinheit mit UNBOUNDED PRECEDING als erstem Trennzeichen verwenden.

Hier sind die Zeitstatistiken für unsere Abfrage:

CPU-Zeit:4297 ms, verstrichene Zeit:4441 ms.

Es dauerte etwa 4,5 Sekunden, bis diese Abfrage auf meinem Computer abgeschlossen war, wobei die Ergebnisse verworfen wurden.

Nun zum Fang. Wenn Sie die Option RANGE anstelle von ROWS mit denselben Trennzeichen verwenden, kann es einen geringfügigen Unterschied in der Bedeutung, aber einen großen Unterschied in der Leistung im Zeilenmodus geben. Der Bedeutungsunterschied ist nur relevant, wenn Sie keine Gesamtordnung haben, dh wenn Sie nach etwas bestellen, das nicht eindeutig ist. Die Option ROWS UNBOUNDED PRECEDING stoppt bei der aktuellen Zeile, sodass die Berechnung im Fall von Unentschieden nicht deterministisch ist. Umgekehrt blickt die Option RANGE UNBOUNDED PRECEDING vor die aktuelle Zeile und schließt gegebenenfalls Haltebögen ein. Es verwendet eine ähnliche Logik wie die Option TOP WITH TIES. Wenn Sie eine Gesamtsortierung haben, d. h. Sie sortieren nach etwas Einzigartigem, müssen keine Bindungen eingefügt werden, und daher werden ROWS und RANGE in einem solchen Fall logisch äquivalent. Das Problem besteht darin, dass SQL Server bei der Verwendung von RANGE bei der Verarbeitung im Zeilenmodus immer den Spool auf der Festplatte verwendet, da er bei der Verarbeitung einer bestimmten Zeile nicht vorhersagen kann, wie viele weitere Zeilen eingeschlossen werden. Dies kann zu schwerwiegenden Leistungseinbußen führen.

Betrachten Sie die folgende Abfrage (nennen Sie sie Abfrage 2), die mit Abfrage 1 identisch ist und nur die Option RANGE anstelle von ROWS verwendet:

 SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid RANGE UNBOUNDED PRECEDING ) AS balance FROM dbo.Transactions;

Der Plan für diese Abfrage ist in Abbildung 2 dargestellt.

Abbildung 2:Plan für Abfrage 2, Verarbeitung im Zeilenmodus

Abfrage 2 ist logisch äquivalent zu Abfrage 1, da wir eine Gesamtreihenfolge haben; Da es jedoch RANGE verwendet, wird es mit dem Spool auf der Festplatte optimiert. Beachten Sie, dass der Fenster-Spool im Plan für Abfrage 2 genauso aussieht wie im Plan für Abfrage 1 und die geschätzten Kosten dieselben sind.

Hier sind die Zeit- und E/A-Statistiken für die Ausführung von Abfrage 2:

CPU-Zeit:19515 ms, verstrichene Zeit:20201 ms.
Logische Lesevorgänge der Tabelle „Worktable“:12044701. Logische Lesevorgänge der Tabelle „Transactions“:6208.

Beachten Sie die große Anzahl logischer Lesevorgänge für Worktable, was darauf hinweist, dass Sie den Spool auf der Festplatte erhalten haben. Die Laufzeit ist mehr als viermal länger als bei Abfrage 1.

Wenn Sie denken, dass Sie in diesem Fall einfach die Option RANGE vermeiden, es sei denn, Sie müssen wirklich Haltebögen einschließen, ist das eine gute Idee. Das Problem ist, dass Sie, wenn Sie eine Fensterfunktion verwenden, die einen Rahmen (Aggregate, FIRST_VALUE, LAST_VALUE) mit einer expliziten Fensterreihenfolgeklausel unterstützt, aber keine Erwähnung der Fensterrahmeneinheit und der zugehörigen Ausdehnung, standardmäßig RANGE UNBOUNDED PRECEDING erhalten . Dieser Standardwert wird vom SQL-Standard vorgegeben, und der Standard hat ihn gewählt, weil er im Allgemeinen deterministischere Optionen als Standardwerte bevorzugt. Die folgende Abfrage (nennen wir sie Abfrage 3) ist ein Beispiel, das in diese Falle tappt:

 SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ) AS balance FROM dbo.Transactions;

Oft schreiben Leute so, in der Annahme, dass sie standardmäßig ROWS UNBOUNDED PRECEDING erhalten, ohne zu wissen, dass sie tatsächlich RANGE UNBOUNDED PRECEDING erhalten. Die Sache ist, dass Sie, da die Funktion die Gesamtreihenfolge verwendet, das gleiche Ergebnis wie bei ROWS erhalten, sodass Sie anhand des Ergebnisses nicht erkennen können, dass ein Problem vorliegt. Aber die Leistungszahlen, die Sie erhalten, sind wie bei Abfrage 2. Ich sehe ständig Leute, die in diese Falle tappen.

Die beste Vorgehensweise zur Vermeidung dieses Problems besteht darin, in Fällen, in denen Sie eine Fensterfunktion mit einem Rahmen verwenden, die Fensterrahmeneinheit und ihre Ausdehnung explizit anzugeben und im Allgemeinen ROWS zu bevorzugen. Reservieren Sie die Verwendung von RANGE nur für Fälle, in denen die Reihenfolge nicht eindeutig ist und Sie Bindungen einfügen müssen.

Betrachten Sie die folgende Abfrage, die einen Fall veranschaulicht, in dem es einen konzeptionellen Unterschied zwischen ROWS und RANGE gibt:

 SELECT orderdate, orderid, val, SUM(val) OVER( ORDER BY orderdate ROWS UNBOUNDED PRECEDING ) AS sumrows, SUM(val) OVER( ORDER BY orderdate RANGE UNBOUNDED PRECEDING ) AS sumrange FROM Sales.OrderValues ​​ORDER BY orderdate;

Diese Abfrage generiert die folgende Ausgabe:

 orderdate orderid val sumrows sumrange ---------- -------- -------- -------- -------- . /pre> 

Beachten Sie den Unterschied in den Ergebnissen für die Zeilen, in denen dasselbe Bestelldatum mehr als einmal vorkommt, wie beim 8. Juli 2017. Beachten Sie, dass die Option ROWS keine Bindungen enthält und daher nicht deterministisch ist, und wie die Option RANGE abschneidet Bindungen enthalten und ist daher immer deterministisch.

Es ist jedoch fraglich, ob Sie in der Praxis Fälle haben, in denen Sie nach etwas ordnen, das nicht eindeutig ist, und Sie wirklich die Einbeziehung von Bindungen benötigen, um die Berechnung deterministisch zu machen. Was in der Praxis wahrscheinlich viel häufiger vorkommt, ist, eines von zwei Dingen zu tun. Eine besteht darin, Bindungen zu lösen, indem Sie etwas zur Fensterreihenfolge hinzufügen, um es einzigartig zu machen, und auf diese Weise zu einer deterministischen Berechnung führen, wie folgt:

 SELECT orderdate, orderid, val, SUM(val) OVER( ORDER BY orderdate, orderid ROWS UNBOUNDED PRECEDING ) AS runningsum FROM Sales.OrderValues ​​ORDER BY orderdate;

Diese Abfrage generiert die folgende Ausgabe:

 orderdate orderid val runningsum ---------- -------- --------- ----------- 2017-07-04 10248 440.00 440.00 2017-07-05 10249 1863.40 2303.40 2017-07-08 10250 1552.60 3856.00 2017-07-08 10251 654.06 4510.06 2017-07-09 10252 3597.90 8107.96 ... 
 Eine weitere Option ist die vorläufige Gruppierung, in unserem Fall nach Bestelldatum, wie folgt:

 SELECT orderdate, SUM(val) AS daytotal, SUM(SUM(val)) OVER( ORDER BY orderdate ROWS UNBOUNDED PRECEDING ) AS runningsum FROM Sales.OrderValues ​​GROUP BY orderdate ORDER BY orderdate;

Diese Abfrage generiert die folgende Ausgabe, in der jedes Bestelldatum nur einmal vorkommt:

 Bestelldatum Tagesgesamtsumme ---------- --------- ----------- 2017-07-04 440,00 440,00 2017-07-05 1863,40 2303,40 2017-07-08 2206,66 4510,06 2017-07-09 3597,90 8107,96 ...

Merken Sie sich auf jeden Fall die Best Practices hier!

Die gute Nachricht ist, dass, wenn Sie SQL Server 2016 oder höher ausführen und einen Columnstore-Index für die Daten vorhanden ist (auch wenn es sich um einen gefälschten gefilterten Columnstore-Index handelt), oder wenn Sie SQL Server 2019 oder höher ausführen, oder In Azure SQL-Datenbank werden unabhängig vom Vorhandensein von Columnstore-Indizes alle drei oben genannten Abfragen mit dem Window Aggregate-Operator im Batchmodus optimiert. Mit diesem Operator werden viele der Verarbeitungsineffizienzen im Zeilenmodus eliminiert. Dieser Operator verwendet überhaupt keinen Spool, daher gibt es kein Problem zwischen In-Memory- und On-Disk-Spool. Es verwendet eine ausgefeiltere Verarbeitung, bei der es mehrere parallele Durchläufe über das Fenster von Zeilen im Speicher sowohl für ROWS als auch für RANGE anwenden kann.

Um die Verwendung der Stapelmodusoptimierung zu demonstrieren, vergewissern Sie sich, dass Ihr Datenbankkompatibilitätsgrad auf 150 oder höher eingestellt ist:

 ALTER DATABASE TSQLV5 SET COMPATIBILITY_LEVEL =150;

Führen Sie Abfrage 1 erneut aus:

 SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ROWS UNBOUNDED PRECEDING ) AS balance FROM dbo.Transactions;

Der Plan für diese Abfrage ist in Abbildung 3 dargestellt.

Abbildung 3:Plan für Abfrage 1, Verarbeitung im Stapelmodus

Hier sind die Leistungsstatistiken, die ich für diese Abfrage erhalten habe:

CPU-Zeit:937 ms, verstrichene Zeit:983 ms.
Logische Lesevorgänge der Tabelle „Transaktionen“:6208.

Die Laufzeit sank auf 1 Sekunde!

Führen Sie Abfrage 2 erneut mit der expliziten RANGE-Option aus:

 SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid RANGE UNBOUNDED PRECEDING ) AS balance FROM dbo.Transactions;

Der Plan für diese Abfrage ist in Abbildung 4 dargestellt.

Abbildung 2:Plan für Abfrage 2, Verarbeitung im Stapelmodus

Hier sind die Leistungsstatistiken, die ich für diese Abfrage erhalten habe:

CPU-Zeit:969 ms, verstrichene Zeit:1048 ms.
Logische Lesevorgänge der Tabelle „Transaktionen“:6208.

Die Leistung ist die gleiche wie bei Abfrage 1.

Führen Sie Abfrage 3 erneut mit der impliziten RANGE-Option aus:

 SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ) AS balance FROM dbo.Transactions;

Der Plan und die Leistungszahlen sind natürlich die gleichen wie bei Abfrage 2.

Wenn Sie fertig sind, führen Sie den folgenden Code aus, um die Leistungsstatistiken zu deaktivieren:

 STATISTIKZEIT EINSTELLEN, IO AUS;

Vergessen Sie auch nicht, die Option Ergebnisse nach Ausführung verwerfen in SSMS zu deaktivieren.

Impliziter Frame mit FIRST_VALUE und LAST_VALUE

Die Funktionen FIRST_VALUE und LAST_VALUE sind versetzte Fensterfunktionen, die einen Ausdruck von der ersten bzw. letzten Zeile im Fensterrahmen zurückgeben. Das Schwierige an ihnen ist, dass Leute, wenn sie sie zum ersten Mal verwenden, oft nicht erkennen, dass sie einen Frame unterstützen, sondern denken, dass sie für die gesamte Partition gelten.

Betrachten Sie den folgenden Versuch, Bestellinformationen sowie die Werte der ersten und letzten Bestellung des Kunden zurückzugeben:

 SELECT custid, orderdate, orderid, val, FIRST_VALUE(val) OVER( PARTITION BY custid ORDER BY orderdate, orderid ) AS firstval, LAST_VALUE(val) OVER( PARTITION BY custid ORDER BY orderdate, orderid ) AS lastval FROM Sales. OrderValues ​​ORDER BY custid, orderdate, orderid;

Wenn Sie fälschlicherweise glauben, dass diese Funktionen auf der gesamten Fensterpartition wirken, was viele Leute glauben, die diese Funktionen zum ersten Mal verwenden, erwarten Sie natürlich, dass FIRST_VALUE den Bestellwert der ersten Bestellung des Kunden zurückgibt, und LAST_VALUE, um den zurückzugeben Bestellwert der letzten Bestellung des Kunden. In der Praxis unterstützen diese Funktionen jedoch einen Rahmen. Zur Erinnerung:Bei Funktionen, die einen Rahmen unterstützen, erhalten Sie standardmäßig RANGE UNBOUNDED PRECEDING, wenn Sie die Fensterreihenfolgeklausel, aber nicht die Fensterrahmeneinheit und die zugehörige Ausdehnung angeben. Mit der FIRST_VALUE-Funktion erhalten Sie das erwartete Ergebnis, aber wenn Ihre Abfrage mit Zeilenmodusoperatoren optimiert wird, müssen Sie den Nachteil zahlen, dass Sie den Spool auf der Festplatte verwenden. Mit der LAST_VALUE-Funktion ist es noch schlimmer. Sie zahlen nicht nur die Strafe für das Spoolen auf der Festplatte, sondern statt den Wert aus der letzten Zeile in der Partition zu erhalten, erhalten Sie den Wert aus der aktuellen Zeile!

Hier ist die Ausgabe der obigen Abfrage:

 custid orderdate orderid val firstval lastval ------- ---------- -------- ---------- ------ ---- ---------- 1 2018-08-25 10643 814,50 814,50 814,50 1 2018-10-03 10692 878,00 814,50 878,00 1 2018-10-13 10702 330,00 814,50 330,019-0 1 12,019-0 10835 845.80 814.50 845.80 1 2019-03-16 10952 471.20 814.50 471.20 1 2019-04-09 11011 933.50 814.50 933.50 2 2017-09-18 10308 88.80 88.80 88.80 2 2018-08-08 10625 479.75 88.80 479.75 2 2018-11-28 10759 320.00 88.80 320.00 2 2019-03-04 10926 514.40 88.80 514.40 3 2017-11-27 10365 403.20 403.20 403.20 3 2018-04-15 10507 749.06 403.20 749.06 3 2018-05-13 10535 1940.85 403.20 1940.85 3 2018-06-19 10573 2082,00 403,20 2082,00 3 2018-09-22 10677 813,37 403,20 813,37 3 2018-09-25 10682 375,50 403,20 375,50 3 2019-01-28 10856 660,00 403,20 660,00 ...

Wenn Leute eine solche Ausgabe zum ersten Mal sehen, denken sie oft, dass SQL Server einen Fehler hat. Aber natürlich nicht; es ist einfach der Standard des SQL-Standards. Es gibt einen Fehler in der Abfrage. Wenn Sie erkennen, dass es sich um einen Frame handelt, sollten Sie die Frame-Spezifikation explizit angeben und den minimalen Frame verwenden, der die gesuchte Zeile erfasst. Stellen Sie außerdem sicher, dass Sie die ROWS-Einheit verwenden. Um also die erste Zeile in der Partition zu erhalten, verwenden Sie die FIRST_VALUE-Funktion mit dem Frame ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Um die letzte Zeile in der Partition zu erhalten, verwenden Sie die Funktion LAST_VALUE mit dem Frame ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING.

Hier ist unsere überarbeitete Abfrage mit behobenem Fehler:

 SELECT custid, orderdate, orderid, val, FIRST_VALUE(val) OVER( PARTITION BY custid ORDER BY orderdate, orderid ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS firstval, LAST_VALUE(val) OVER( PARTITION BY custid ORDER BY orderdate, orderid ZEILEN ZWISCHEN DER AKTUELLEN ZEILE UND UNBEGRENZTEN FOLGENDEN ) AS lastval FROM Sales.OrderValues ​​ORDER BY custid, orderdate, orderid;

Diesmal erhalten Sie das richtige Ergebnis:

 custid orderdate orderid val firstval lastval ------- ---------- -------- ---------- ------ ---- ---------- 1 2018-08-25 10643 814,50 814,50 933,50 1 2018-10-03 10692 878,00 814,50 933,50 1 2018-10-13 10702 330,00 814,50 933,50 1 1,0 10835 845.80 814.50 933.50 1 2019-03-16 10952 471.20 814.50 933.50 1 2019-04-09 11011 933.50 814.50 933.50 2 2017-09-18 10308 88.80 88.80 514.40 2 2018-08-08 10625 479.75 88.80 514.40 2 2018-11-28 10759 320.00 88.80 514.40 2 2019-03-04 10926 514.40 88.80 514.40 3 2017-11-27 10365 403.20 403.20 660.00 3 2018-04-15 10507 749.06 403.20 660.00 3 2018-05-13 10535 1940.85 403.20 660.00 3 2018-06-19 10573 2082,00 403,20 660,00 3 2018-09-22 10677 813,37 403,20 660,00 3 2018-09-25 10682 375,50 403,20 660,00 3 2019-01-28 10856 660,00 403,20 660,00 ...

Man fragt sich, was die Motivation für den Standard war, einen Rahmen mit diesen Funktionen überhaupt zu unterstützen. Wenn Sie darüber nachdenken, werden Sie sie hauptsächlich verwenden, um etwas aus der ersten oder letzten Zeile in der Partition zu erhalten. Wenn Sie den Wert von beispielsweise zwei Zeilen vor dem aktuellen Wert benötigen, ist es nicht viel einfacher, LAG mit einem expliziten Offset von 2 zu verwenden, anstatt FIRST_VALUE mit einem Frame zu verwenden, der mit 2 PRECEDING beginnt:

 SELECT custid, orderdate, orderid, val, LAG(val, 2) OVER( PARTITION BY custid ORDER BY orderdate, orderid ) AS prevtwoval FROM Sales.OrderValues ​​ORDER BY custid, orderdate, orderid;

Diese Abfrage generiert die folgende Ausgabe:

 custid orderdate orderid val prevtwoval ------- ---------- -------- ---------- ------- ---- 1 2018-08-25 10643 814.50 NULL 1 2018-10-03 10692 878.00 NULL 1 2018-10-13 10702 330.00 814.50 1 2019-01-15 10835 845.80 878.00 1 2019-03-16 10952 471.20 330.00 1 2019-04-09 11011 933.50 845.80 2 2017-09-18 10308 88.80 NULL 2 2018-08-08 10625 479.75 NULL 2 2018-11-28 10759 320.00 88.80 2 2019-03-04 10926 514.40 479.75 3 2017-11-27 10365 403.20 NULL 3 2018-04-15 10507 749.06 NULL 3 2018-05-13 10535 1940.85 403.20 3 2018-06-19 10573 2082.00 749.06 3 2018-09-22 10677 813.37 1940.85 3 2018-09-25 10682 375.50 2082.00 3 2019 -01-28 10856 660,00 813,37 ...

Anscheinend gibt es einen semantischen Unterschied zwischen der obigen Verwendung der LAG-Funktion und FIRST_VALUE mit einem Rahmen, der mit 2 PRECEDING beginnt. Bei Ersterem erhalten Sie standardmäßig NULL, wenn eine Zeile im gewünschten Offset nicht vorhanden ist. Bei letzterem erhalten Sie immer noch den Wert aus der ersten Zeile, die vorhanden ist, also den Wert aus der ersten Zeile in der Partition. Betrachten Sie die folgende Abfrage:

 SELECT custid, orderdate, orderid, val, FIRST_VALUE(val) OVER( PARTITION BY custid ORDER BY orderdate, orderid ROWS BETWEEN 2 PRECEDING AND CURRENT ROW ) AS prevtwoval FROM Sales.OrderValues ​​ORDER BY custid, orderdate, orderid; 

Diese Abfrage generiert die folgende Ausgabe:

 custid orderdate orderid val prevtwoval ------- ---------- -------- ---------- ------- ---- 1 2018-08-25 10643 814.50 814.50 1 2018-10-03 10692 878.00 814.50 1 2018-10-13 10702 330.00 814.50 1 2019-01-15 10835 845.80 878.00 1 2019-03-16 10952 471.20 330.00 1 2019-04-09 11011 933.50 845.80 2 2017-09-18 10308 88.80 88.80 2 2018-08-08 10625 479.75 88.80 2 2018-11-28 10759 320.00 88.80 2 2019-03-04 10926 514.40 479.75 3 2017-11-27 10365 403.20 403.20 3 2018-04-15 10507 749.06 403.20 3 2018-05-13 10535 1940.85 403.20 3 2018-06-19 10573 2082.00 749.06 3 2018-09-22 10677 813.37 1940.85 3 2018-09-25 10682 375.50 2082.00 3 2019 -01-28 10856 660,00 813,37 ...

Beachten Sie, dass diesmal keine NULLen in der Ausgabe sind. Es ist also sinnvoll, einen Frame mit FIRST_VALUE und LAST_VALUE zu unterstützen. Stellen Sie nur sicher, dass Sie sich an die bewährte Methode erinnern, bei diesen Funktionen immer die Frame-Spezifikation explizit anzugeben und die ROWS-Option mit dem minimalen Frame zu verwenden, der die Zeile enthält, nach der Sie suchen.

Schlussfolgerung

Dieser Artikel konzentrierte sich auf Fehler, Fallstricke und Best Practices im Zusammenhang mit Fensterfunktionen. Denken Sie daran, dass sowohl Fensteraggregatfunktionen als auch die Fensteroffsetfunktionen FIRST_VALUE und LAST_VALUE einen Frame unterstützen und dass Sie, wenn Sie die Window-Order-Klausel, aber nicht die Window-Frame-Einheit und die zugehörige Ausdehnung angeben, RANGE UNBOUNDED PRECEDING von erhalten Ursprünglich. Dies führt zu Leistungseinbußen, wenn die Abfrage mit Zeilenmodusoperatoren optimiert wird. Mit der LAST_VALUE-Funktion führt dies dazu, dass die Werte aus der aktuellen Zeile statt aus der letzten Zeile in der Partition abgerufen werden. Denken Sie daran, den Rahmen explizit anzugeben und im Allgemeinen die Option ROWS der Option RANGE vorzuziehen. Es ist großartig, die Leistungsverbesserungen mit dem Window Aggregate-Operator im Stapelmodus zu sehen. Wenn es anwendbar ist, wird zumindest die Leistungsfalle beseitigt.