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

Verschachtelte Fensterfunktionen in SQL

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:

(ROW_NUMBER()>) OVER()

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:

( VALUE OF AT [] [, ]
>) 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!