Der ISO/IEC 9075:2016-Standard (SQL:2016) definiert eine Funktion namens verschachtelte Fensterfunktionen. Mit dieser Funktion können Sie zwei Arten von Fensterfunktionen als Argument einer Fensteraggregatfunktion verschachteln. Die Idee ist, Ihnen zu ermöglichen, an strategischen Markierungen in Fensterelementen entweder auf eine Zeilennummer oder auf einen Wert eines Ausdrucks zu verweisen. Die Markierungen geben Ihnen Zugriff auf die erste oder letzte Reihe in der Partition, die erste oder letzte Reihe im Rahmen, die aktuelle äußere Reihe und die aktuelle Rahmenreihe. Diese Idee ist sehr mächtig und ermöglicht es Ihnen, Filter und andere Arten von Manipulationen innerhalb Ihrer Fensterfunktion anzuwenden, die sonst manchmal schwer zu erreichen sind. Sie können auch verschachtelte Fensterfunktionen verwenden, um andere Funktionen, wie z. B. RANGE-basierte Frames, einfach zu emulieren. Diese Funktion ist derzeit in T-SQL nicht verfügbar. Ich habe einen Vorschlag zur Verbesserung von SQL Server gepostet, indem Unterstützung für verschachtelte Fensterfunktionen hinzugefügt wurde. Stellen Sie sicher, dass Sie Ihre Stimme abgeben, wenn Sie der Meinung sind, dass diese Funktion für Sie von Vorteil sein könnte.
Worum es bei verschachtelten Fensterfunktionen nicht geht
Zum Zeitpunkt der Erstellung dieses Artikels sind nicht viele Informationen über die echten Standardfunktionen für verschachtelte Fenster verfügbar. Was es schwieriger macht, ist, dass ich noch keine Plattform kenne, die diese Funktion implementiert hat. Tatsächlich liefert das Ausführen einer Websuche nach verschachtelten Fensterfunktionen hauptsächlich Abdeckung und Diskussionen über das Verschachteln von gruppierten Aggregatfunktionen in gefensterten Aggregatfunktionen. Angenommen, Sie möchten die Sales.OrderValues-Ansicht in der TSQLV5-Beispieldatenbank abfragen und für jeden Kunden und jedes Bestelldatum die tägliche Summe der Bestellwerte und die laufende Summe bis zum aktuellen Tag zurückgeben. Eine solche Aufgabe beinhaltet sowohl das Gruppieren als auch das Fenstern. Sie gruppieren die Zeilen nach der Kunden-ID und dem Bestelldatum und wenden eine laufende Summe auf die Gruppensumme der Bestellwerte an, etwa so:
USE TSQLV5; -- http://tsql.solidq.com/SampleDatabases/TSQLV5.zip SELECT custid, orderdate, SUM(val) AS daytotal, SUM(SUM(val)) OVER(PARTITION BY custid ORDER BY orderdate ROWS UNBOUNDED PRECEDING) AS runningsum FROM Sales.OrderValues GROUP BY custid, orderdate;
Diese Abfrage erzeugt die folgende Ausgabe, hier in abgekürzter Form dargestellt:
custid orderdate daytotal runningsum ------- ---------- -------- ---------- 1 2018-08-25 814.50 814.50 1 2018-10-03 878.00 1692.50 1 2018-10-13 330.00 2022.50 1 2019-01-15 845.80 2868.30 1 2019-03-16 471.20 3339.50 1 2019-04-09 933.50 4273.00 2 2017-09-18 88.80 88.80 2 2018-08-08 479.75 568.55 2 2018-11-28 320.00 888.55 2 2019-03-04 514.40 1402.95 ...
Auch wenn diese Technik ziemlich cool ist und obwohl Websuchen nach verschachtelten Fensterfunktionen hauptsächlich solche Techniken zurückgeben, ist das nicht das, was der SQL-Standard mit verschachtelten Fensterfunktionen meint. Da ich dort keine Informationen zu dem Thema finden konnte, musste ich es einfach aus der Norm selbst herausfinden. Hoffentlich wird dieser Artikel das Bewusstsein für das Feature der echten verschachtelten Fensterfunktionen schärfen und dazu führen, dass sich die Leute an Microsoft wenden und darum bitten, es in SQL Server zu unterstützen.
Worum es bei verschachtelten Fensterfunktionen geht
Verschachtelte Fensterfunktionen umfassen zwei Funktionen, die Sie als Argument einer Fensteraggregatfunktion verschachteln können. Dies sind die verschachtelte Zeilennummerfunktion und der verschachtelte value_of-Ausdruck bei der Zeilenfunktion.
Verschachtelte Zeilennummerfunktion
Die verschachtelte Zeilennummerfunktion ermöglicht es Ihnen, auf die Zeilennummer strategischer Markierungen in Fensterelementen zu verweisen. Hier ist die Syntax der Funktion:
Die Zeilenmarkierungen, die Sie angeben können, sind:
- BEGIN_PARTITION
- END_PARTITION
- BEGIN_FRAME
- END_FRAME
- AKTUELLE_ZEILE
- FRAME_ROW
Die ersten vier Markierungen sind selbsterklärend. Wie bei den letzten beiden repräsentiert die Markierung CURRENT_ROW die aktuelle äußere Reihe und die FRAME_ROW die aktuelle innere Rahmenreihe.
Betrachten Sie als Beispiel für die Verwendung der verschachtelten Zeilennummerfunktion die folgende Aufgabe. Sie müssen die Sales.OrderValues-Ansicht abfragen und für jede Bestellung einige ihrer Attribute sowie die Differenz zwischen dem aktuellen Bestellwert und dem Kundendurchschnitt zurückgeben, aber die erste und letzte Kundenbestellung aus dem Durchschnitt ausschließen.
Diese Aufgabe ist ohne verschachtelte Fensterfunktionen machbar, aber die Lösung umfasst einige Schritte:
WITH C1 AS ( SELECT custid, val, ROW_NUMBER() OVER( PARTITION BY custid ORDER BY orderdate, orderid ) AS rownumasc, ROW_NUMBER() OVER( PARTITION BY custid ORDER BY orderdate DESC, orderid DESC ) AS rownumdesc FROM Sales.OrderValues ), C2 AS ( SELECT custid, AVG(val) AS avgval FROM C1 WHERE 1 NOT IN (rownumasc, rownumdesc) GROUP BY custid ) SELECT O.orderid, O.custid, O.orderdate, O.val, O.val - C2.avgval AS diff FROM Sales.OrderValues AS O LEFT OUTER JOIN C2 ON O.custid = C2.custid;
Hier ist die Ausgabe dieser Abfrage, hier in abgekürzter Form dargestellt:
orderid custid orderdate val diff -------- ------- ---------- -------- ------------ 10411 10 2018-01-10 966.80 -570.184166 10743 4 2018-11-17 319.20 -809.813636 11075 68 2019-05-06 498.10 -1546.297500 10388 72 2017-12-19 1228.80 -358.864285 10720 61 2018-10-28 550.00 -144.744285 11052 34 2019-04-27 1332.00 -1164.397500 10457 39 2018-02-25 1584.00 -797.999166 10789 23 2018-12-22 3687.00 1567.833334 10434 24 2018-02-03 321.12 -1329.582352 10766 56 2018-12-05 2310.00 1015.105000 ...
Unter Verwendung von verschachtelten Zeilennummernfunktionen ist die Aufgabe mit einer einzigen Abfrage erreichbar, etwa so:
SELECT orderid, custid, orderdate, val, val - AVG( CASE WHEN ROW_NUMBER(FRAME_ROW) NOT IN ( ROW_NUMBER(BEGIN_PARTITION), ROW_NUMBER(END_PARTITION) ) THEN val END ) OVER( PARTITION BY custid ORDER BY orderdate, orderid ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ) AS diff FROM Sales.OrderValues;
Außerdem erfordert die derzeit unterstützte Lösung mindestens eine Sortierung im Plan und mehrere Datendurchläufe. Die Lösung, die verschachtelte Zeilennummernfunktionen verwendet, hat das gesamte Potenzial, optimiert zu werden, indem man sich auf die Indexreihenfolge und eine reduzierte Anzahl von Datenübergängen verlässt. Dies ist jedoch natürlich implementierungsabhängig.
Verschachtelter value_of-Ausdruck bei Zeilenfunktion
Die verschachtelte Funktion value_of expression at row ermöglicht es Ihnen, mit einem Wert eines Ausdrucks an denselben strategischen Zeilenmarkierungen zu interagieren, die zuvor in einem Argument einer Fensteraggregatfunktion erwähnt wurden. Hier ist die Syntax dieser Funktion:
>) OVER(
Wie Sie sehen, können Sie ein bestimmtes negatives oder positives Delta in Bezug auf die Zeilenmarkierung angeben und optional einen Standardwert angeben, falls eine Zeile an der angegebenen Position nicht vorhanden ist.
Diese Fähigkeit gibt Ihnen viel Kraft, wenn Sie mit verschiedenen Punkten in Fensterelementen interagieren müssen. Bedenken Sie, dass so leistungsfähig Fensterfunktionen mit alternativen Tools wie Unterabfragen verglichen werden können, was Fensterfunktionen nicht unterstützen, ist ein grundlegendes Konzept einer Korrelation. Mit dem Marker CURRENT_ROW erhalten Sie Zugriff auf die äußere Zeile und emulieren so Korrelationen. Gleichzeitig profitieren Sie von allen Vorteilen, die Fensterfunktionen gegenüber Unterabfragen haben.
Angenommen, Sie müssen die Ansicht „Sales.OrderValues“ abfragen und für jede Bestellung einige ihrer Attribute sowie die Differenz zwischen dem aktuellen Bestellwert und dem Kundendurchschnitt zurückgeben, jedoch ohne Bestellungen, die am selben Datum wie aufgegeben wurden das aktuelle Bestelldatum. Dies erfordert eine Fähigkeit ähnlich einer Korrelation. Mit der verschachtelten value_of expression at row-Funktion ist dies unter Verwendung des CURRENT_ROW-Markers einfach wie folgt zu erreichen:
SELECT orderid, custid, orderdate, val, val - AVG( CASE WHEN orderdate <> VALUE OF orderdate AT CURRENT_ROW THEN val END ) OVER( PARTITION BY custid ) AS diff FROM Sales.OrderValues;
Diese Abfrage soll die folgende Ausgabe erzeugen:
orderid custid orderdate val diff -------- ------- ---------- -------- ------------ 10248 85 2017-07-04 440.00 180.000000 10249 79 2017-07-05 1863.40 1280.452000 10250 34 2017-07-08 1552.60 -854.228461 10251 84 2017-07-08 654.06 -293.536666 10252 76 2017-07-09 3597.90 1735.092728 10253 34 2017-07-10 1444.80 -970.320769 10254 14 2017-07-11 556.62 -1127.988571 10255 68 2017-07-12 2490.50 617.913334 10256 88 2017-07-15 517.80 -176.000000 10257 35 2017-07-16 1119.90 -153.562352 ...
Wenn Sie denken, dass diese Aufgabe genauso einfach mit korrelierten Unterabfragen zu lösen ist, liegen Sie in diesem vereinfachten Fall richtig. Dasselbe kann mit der folgenden Abfrage erreicht werden:
SELECT O1.orderid, O1.custid, O1.orderdate, O1.val, O1.val - ( SELECT AVG(O2.val) FROM Sales.OrderValues AS O2 WHERE O2.custid = O1.custid AND O2.orderdate <> O1.orderdate ) AS diff FROM Sales.OrderValues AS O1;
Denken Sie jedoch daran, dass eine Unterabfrage mit einer unabhängigen Ansicht der Daten arbeitet, während eine Fensterfunktion mit der Menge arbeitet, die als Eingabe für den logischen Abfrageverarbeitungsschritt bereitgestellt wird, der die SELECT-Klausel verarbeitet. Normalerweise verfügt die zugrunde liegende Abfrage über zusätzliche Logik wie Verknüpfungen, Filter, Gruppierung und dergleichen. Bei Unterabfragen müssen Sie entweder einen vorläufigen CTE vorbereiten oder die Logik der zugrunde liegenden Abfrage auch in der Unterabfrage wiederholen. Bei Fensterfunktionen muss die Logik nicht wiederholt werden.
Angenommen, Sie sollten nur versandte Bestellungen bearbeiten (wobei das Versanddatum nicht NULL ist), die von Mitarbeiter 3 bearbeitet wurden. Die Lösung mit der Fensterfunktion muss die Filterprädikate nur einmal hinzufügen, etwa so:
SELECT orderid, custid, orderdate, val, val - AVG( CASE WHEN orderdate <> VALUE OF orderdate AT CURRENT_ROW THEN val END ) OVER( PARTITION BY custid ) AS diff FROM Sales.OrderValues WHERE empid = 3 AND shippeddate IS NOT NULL;
Diese Abfrage soll die folgende Ausgabe erzeugen:
orderid custid orderdate val diff -------- ------- ---------- -------- ------------- 10251 84 2017-07-08 654.06 -459.965000 10253 34 2017-07-10 1444.80 531.733334 10256 88 2017-07-15 517.80 -1022.020000 10266 87 2017-07-26 346.56 NULL 10273 63 2017-08-05 2037.28 -3149.075000 10283 46 2017-08-16 1414.80 534.300000 10309 37 2017-09-19 1762.00 -1951.262500 10321 38 2017-10-03 144.00 NULL 10330 46 2017-10-16 1649.00 885.600000 10332 51 2017-10-17 1786.88 495.830000 ...
Die Lösung mit der Unterabfrage muss die Filterprädikate zweimal hinzufügen – einmal in der äußeren Abfrage und einmal in der Unterabfrage – etwa so:
SELECT O1.orderid, O1.custid, O1.orderdate, O1.val, O1.val - ( SELECT AVG(O2.val) FROM Sales.OrderValues AS O2 WHERE O2.custid = O1.custid AND O2.orderdate <> O1.orderdate AND empid = 3 AND shippeddate IS NOT NULL) AS diff FROM Sales.OrderValues AS O1 WHERE empid = 3 AND shippeddate IS NOT NULL;
Entweder dies oder das Hinzufügen eines vorläufigen CTE, der sich um die gesamte Filterung und andere Logik kümmert. Wie auch immer Sie es betrachten, bei Unterabfragen sind mehr Komplexitätsschichten beteiligt.
Der andere Vorteil von verschachtelten Fensterfunktionen besteht darin, dass es einfach gewesen wäre, die fehlende vollständige Unterstützung für die RANGE-Fensterrahmeneinheit zu emulieren, wenn wir Unterstützung für die in T-SQL gehabt hätten. Die Option RANGE soll es Ihnen ermöglichen, dynamische Frames zu definieren, die auf einem Offset vom Ordnungswert in der aktuellen Zeile basieren. Angenommen, Sie müssen für jede Kundenbestellung aus der Ansicht „Sales.OrderValues“ den gleitenden Durchschnittswert der letzten 14 Tage berechnen. Nach dem SQL-Standard können Sie dies mit der RANGE-Option und dem INTERVAL-Typ wie folgt erreichen:
SELECT orderid, custid, orderdate, val, AVG(val) OVER( PARTITION BY custid ORDER BY orderdate RANGE BETWEEN INTERVAL '13' DAY PRECEDING AND CURRENT ROW ) AS movingavg14days FROM Sales.OrderValues;
Diese Abfrage soll die folgende Ausgabe erzeugen:
orderid custid orderdate val movingavg14days -------- ------- ---------- ------- --------------- 10643 1 2018-08-25 814.50 814.500000 10692 1 2018-10-03 878.00 878.000000 10702 1 2018-10-13 330.00 604.000000 10835 1 2019-01-15 845.80 845.800000 10952 1 2019-03-16 471.20 471.200000 11011 1 2019-04-09 933.50 933.500000 10308 2 2017-09-18 88.80 88.800000 10625 2 2018-08-08 479.75 479.750000 10759 2 2018-11-28 320.00 320.000000 10926 2 2019-03-04 514.40 514.400000 10365 3 2017-11-27 403.20 403.200000 10507 3 2018-04-15 749.06 749.060000 10535 3 2018-05-13 1940.85 1940.850000 10573 3 2018-06-19 2082.00 2082.000000 10677 3 2018-09-22 813.37 813.370000 10682 3 2018-09-25 375.50 594.435000 10856 3 2019-01-28 660.00 660.000000 ...
Zum Zeitpunkt der Erstellung dieses Dokuments wird diese Syntax in T-SQL nicht unterstützt. Hätten wir Unterstützung für verschachtelte Fensterfunktionen in T-SQL gehabt, hätten Sie diese Abfrage mit dem folgenden Code emulieren können:
SELECT orderid, custid, orderdate, val, AVG( CASE WHEN DATEDIFF(day, orderdate, VALUE OF orderdate AT CURRENT_ROW) BETWEEN 0 AND 13 THEN val END ) OVER( PARTITION BY custid ORDER BY orderdate RANGE UNBOUNDED PRECEDING ) AS movingavg14days FROM Sales.OrderValues;
Was kann ich nicht mögen?
Geben Sie Ihre Stimme ab
Die standardmäßigen verschachtelten Fensterfunktionen scheinen ein sehr leistungsfähiges Konzept zu sein, das viel Flexibilität bei der Interaktion mit verschiedenen Punkten in Fensterelementen ermöglicht. Ich bin ziemlich überrascht, dass ich außer im Standard selbst keine Abdeckung des Konzepts finden kann und dass ich nicht viele Plattformen sehe, die es implementieren. Hoffentlich wird dieser Artikel das Bewusstsein für diese Funktion steigern. Wenn Sie der Meinung sind, dass es für Sie nützlich sein könnte, es in T-SQL verfügbar zu haben, geben Sie unbedingt Ihre Stimme ab!