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:
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:
>) 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!