sql >> Database >  >> RDS >> Database

Geneste vensterfuncties in SQL

De ISO/IEC 9075:2016-standaard (SQL:2016) definieert een functie die geneste vensterfuncties wordt genoemd. Met deze functie kunt u twee soorten vensterfuncties nesten als argument van een vensteraggregatiefunctie. Het idee is om u toe te staan ​​te verwijzen naar een rijnummer, of naar een waarde van een uitdrukking, bij strategische markeringen in vensterelementen. De markeringen geven u toegang tot de eerste of laatste rij in de partitie, de eerste of laatste rij in het frame, de huidige buitenste rij en de huidige framerij. Dit idee is erg krachtig, waardoor je filtering en andere soorten manipulaties binnen je vensterfunctie kunt toepassen die anders soms moeilijk te bereiken zijn. U kunt ook geneste vensterfuncties gebruiken om gemakkelijk andere functies te emuleren, zoals op RANGE gebaseerde frames. Deze functie is momenteel niet beschikbaar in T-SQL. Ik heb een suggestie gepost om SQL Server te verbeteren door ondersteuning voor geneste vensterfuncties toe te voegen. Zorg ervoor dat u uw stem toevoegt als u denkt dat deze functie nuttig voor u kan zijn.

Waar gaan geneste vensterfuncties niet over

Op de datum van dit schrijven is er niet veel informatie beschikbaar over de echte standaard geneste vensterfuncties. Wat het moeilijker maakt, is dat ik nog geen platform ken dat deze functie heeft geïmplementeerd. In feite levert het uitvoeren van een zoekopdracht op het web voor geneste vensterfuncties meestal dekking van en discussies over het nesten van gegroepeerde aggregatiefuncties binnen gevensterde aggregatiefuncties. Stel dat u de weergave Sales.OrderValues ​​in de TSQLV5-voorbeelddatabase wilt opvragen en voor elke klant en besteldatum het dagelijkse totaal van de bestelwaarden en het lopende totaal tot de huidige dag wilt retourneren. Een dergelijke taak omvat zowel groeperen als vensteren. U groepeert de rijen op klant-ID en de besteldatum en past een lopende som toe bovenop de groepssom van de bestelwaarden, zoals:

  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;

Deze query genereert de volgende uitvoer, hier in verkorte vorm weergegeven:

  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
  ...

Hoewel deze techniek best cool is, en hoewel zoekopdrachten op het web naar geneste vensterfuncties voornamelijk dergelijke technieken opleveren, is dat niet wat de SQL-standaard bedoelt met geneste vensterfuncties. Omdat ik daar geen informatie over het onderwerp kon vinden, moest ik het gewoon uit de standaard zelf uitzoeken. Hopelijk vergroot dit artikel het bewustzijn van de functie van echte geneste vensterfuncties en zorgt het ervoor dat mensen zich tot Microsoft wenden en vragen om ondersteuning ervoor in SQL Server.

Waar gaan geneste vensterfuncties over

Geneste vensterfuncties bevatten twee functies die u kunt nesten als argument van een vensteraggregatiefunctie. Dat zijn de geneste rijnummerfunctie en de geneste waarde_van expressie bij rijfunctie.

Functie geneste rijnummer

Met de geneste rijnummerfunctie kunt u verwijzen naar het rijnummer van strategische markeringen in vensterelementen. Dit is de syntaxis van de functie:

(ROW_NUMBER()>) OVER()

De rijmarkeringen die u kunt specificeren zijn:

  • BEGIN_PARTITION
  • END_PARTITION
  • BEGIN_FRAME
  • END_FRAME
  • CURRENT_ROW
  • FRAME_ROW

De eerste vier markeringen spreken voor zich. Wat de laatste twee betreft, vertegenwoordigt de CURRENT_ROW-markering de huidige buitenste rij en de FRAME_ROW de huidige binnenste framerij.

Als voorbeeld voor het gebruik van de functie geneste rijnummers, kunt u de volgende taak overwegen. U moet de weergave Sales.OrderValues ​​opvragen en voor elke bestelling enkele van zijn kenmerken retourneren, evenals het verschil tussen de huidige bestelwaarde en het klantgemiddelde, maar exclusief de eerste en laatste klantorders van het gemiddelde.

Deze taak is haalbaar zonder geneste vensterfuncties, maar de oplossing omvat nogal wat stappen:

  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;

Dit is de uitvoer van deze zoekopdracht, hier in verkorte vorm weergegeven:

  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
  ...

Met behulp van geneste rijnummerfuncties is de taak haalbaar met een enkele zoekopdracht, zoals:

  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;

Ook vereist de momenteel ondersteunde oplossing ten minste één sortering in het plan en meerdere passages over de gegevens. De oplossing die gebruikmaakt van geneste rijnummerfuncties heeft alle potentie om geoptimaliseerd te worden door te vertrouwen op indexvolgorde en een verminderd aantal passages over de gegevens. Dit is natuurlijk wel afhankelijk van de implementatie.

Geneste waarde_van expressie bij rijfunctie

De geneste value_of expression at row-functie stelt u in staat om te interageren met een waarde van een expressie op dezelfde strategische rijmarkeringen die eerder zijn genoemd in een argument van een window-aggregatiefunctie. Dit is de syntaxis van deze functie:

( WAARDE VAN AT [] [, ]
>) OVER()

Zoals u kunt zien, kunt u een bepaalde negatieve of positieve delta specificeren met betrekking tot de rijmarkering en optioneel een standaardwaarde opgeven voor het geval een rij niet bestaat op de opgegeven positie.

Deze mogelijkheid geeft je veel kracht wanneer je moet communiceren met verschillende punten in vensterelementen. Bedenk dat hoe krachtig vensterfuncties ook kunnen worden vergeleken met alternatieve tools zoals subquery's, maar wat vensterfuncties niet ondersteunen, is een basisconcept van een correlatie. Met behulp van de CURRENT_ROW-markering krijgt u toegang tot de buitenste rij en emuleert u op deze manier correlaties. Tegelijkertijd profiteert u van alle voordelen die vensterfuncties hebben in vergelijking met subquery's.

Stel dat u bijvoorbeeld de weergave Sales.OrderValues ​​moet opvragen en voor elke bestelling enkele van zijn kenmerken moet retourneren, evenals het verschil tussen de huidige bestelwaarde en het klantgemiddelde, maar met uitzondering van bestellingen die op dezelfde datum zijn geplaatst als de huidige besteldatum. Dit vereist een vermogen vergelijkbaar met een correlatie. Met de geneste value_of expression at row-functie, met behulp van de CURRENT_ROW-markering, is dit eenvoudig als volgt te bereiken:

  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;

Deze query zou de volgende output moeten genereren:

  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
  ...

Als u denkt dat deze taak net zo gemakkelijk haalbaar is met gecorreleerde subquery's, heeft u in dit simplistische geval gelijk. Hetzelfde kan worden bereikt met de volgende vraag:

  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;

Onthoud echter dat een subquery werkt op een onafhankelijke weergave van de gegevens, terwijl een vensterfunctie werkt op de set die wordt geleverd als invoer voor de logische stap voor het verwerken van query's die de SELECT-component afhandelt. Meestal heeft de onderliggende query extra logica zoals joins, filters, groepering en dergelijke. Met subquery's moet u ofwel een voorlopige CTE voorbereiden, of de logica van de onderliggende query ook in de subquery herhalen. Met vensterfuncties is het niet nodig om de logica te herhalen.

Stel bijvoorbeeld dat u alleen zou werken met verzonden bestellingen (waarbij de verzenddatum niet NULL is) die zijn afgehandeld door medewerker 3. De oplossing met de vensterfunctie hoeft de filterpredikaten slechts één keer toe te voegen, zoals:

   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;

Deze query zou de volgende output moeten genereren:

  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
  ...

De oplossing met de subquery moet de filterpredikaten twee keer toevoegen - een keer in de buitenste zoekopdracht en een keer in de subquery - zoals:

  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;

Het is dit, of het toevoegen van een voorlopige CTE die zorgt voor alle filtering en andere logica. Hoe je het ook bekijkt, met subquery's zijn er meer complexiteitslagen bij betrokken.

Het andere voordeel van geneste vensterfuncties is dat als we ondersteuning hadden voor die in T-SQL, het gemakkelijk zou zijn geweest om de ontbrekende volledige ondersteuning voor de RANGE raamkozijneenheid te emuleren. De RANGE-optie zou u in staat moeten stellen dynamische frames te definiëren die zijn gebaseerd op een offset van de bestelwaarde in de huidige rij. Stel bijvoorbeeld dat u voor elke klantorder uit de Sales.OrderValues-weergave de voortschrijdende gemiddelde waarde van de afgelopen 14 dagen moet berekenen. Volgens de SQL-standaard kunt u dit bereiken met behulp van de RANGE-optie en het INTERVAL-type, als volgt:

  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;

Deze query zou de volgende output moeten genereren:

  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
  ...

Op het moment van schrijven wordt deze syntaxis niet ondersteund in T-SQL. Als we ondersteuning hadden gehad voor geneste vensterfuncties in T-SQL, had je deze query kunnen emuleren met de volgende code:

  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;

Wat is er niet leuk aan?

Breng uw stem uit

De standaard geneste vensterfuncties lijken een zeer krachtig concept dat veel flexibiliteit mogelijk maakt in interactie met verschillende punten in vensterelementen. Ik ben nogal verrast dat ik geen andere dekking van het concept kan vinden dan in de standaard zelf, en dat ik niet veel platforms zie die het implementeren. Hopelijk zal dit artikel de bekendheid van deze functie vergroten. Als je denkt dat het nuttig voor je kan zijn om het beschikbaar te hebben in T-SQL, breng dan zeker je stem uit!


  1. Hoe de standaard MySQL/MariaDB-poort in Linux te wijzigen

  2. Fout bij gebruik van oracle.dataaccess.dll

  3. Is het mogelijk om het schema op te geven bij het verbinden met postgres met JDBC?

  4. foreach %dopar% + RPostgreSQL