ISO/IEC 9075:2016-standarden (SQL:2016) definerer en funktion kaldet indlejrede vinduesfunktioner. Denne funktion giver dig mulighed for at indlejre to slags vinduesfunktioner som et argument for en aggregeret vinduesfunktion. Ideen er at give dig mulighed for at henvise til enten et rækkenummer eller til en værdi af et udtryk ved strategiske markører i vindueselementer. Markørerne giver dig adgang til den første eller sidste række i partitionen, den første eller sidste række i rammen, den aktuelle ydre række og den aktuelle rammerække. Denne idé er meget kraftfuld og gør det muligt for dig at anvende filtrering og andre former for manipulationer i din vinduesfunktion, som nogle gange er svære at opnå ellers. Du kan også bruge indlejrede vinduesfunktioner til nemt at efterligne andre funktioner, såsom RANGE-baserede rammer. Denne funktion er i øjeblikket ikke tilgængelig i T-SQL. Jeg sendte et forslag til forbedring af SQL Server ved at tilføje understøttelse af indlejrede vinduesfunktioner. Sørg for at tilføje din stemme, hvis du føler, at denne funktion kan være til gavn for dig.
Hvad indlejrede vinduesfunktioner ikke handler om
På datoen for denne skrivning er der ikke meget information tilgængelig derude om de ægte standard indlejrede vinduesfunktioner. Hvad der gør det sværere er, at jeg endnu ikke kender nogen platform, der implementerede denne funktion. Faktisk returnerer en websøgning efter indlejrede vinduesfunktioner for det meste dækning af og diskussioner om indlejring af grupperede aggregerede funktioner i vinduesbaserede aggregerede funktioner. Antag for eksempel, at du vil forespørge på visningen Sales.OrderValues i TSQLV5-eksempeldatabasen og returnere for hver kunde og ordredato, den daglige total af ordreværdierne og den løbende total indtil den aktuelle dag. En sådan opgave involverer både gruppering og vinduesdeling. Du grupperer rækkerne efter kunde-id og ordredato og anvender en løbende sum oven på gruppesummen af ordreværdierne, som sådan:
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;
Denne forespørgsel genererer følgende output, vist her i forkortet form:
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 ...
Selvom denne teknik er ret cool, og selvom websøgninger efter indlejrede vinduesfunktioner hovedsageligt returnerer sådanne teknikker, er det ikke, hvad SQL-standarden mener med indlejrede vinduesfunktioner. Da jeg ikke kunne finde nogen information derude om emnet, måtte jeg bare finde ud af det ud fra selve standarden. Forhåbentlig vil denne artikel øge bevidstheden om den ægte indlejrede vinduesfunktion og få folk til at henvende sig til Microsoft og bede om at tilføje support til det i SQL Server.
Hvad handler indlejrede vinduesfunktioner om
Indlejrede vinduesfunktioner omfatter to funktioner, som du kan indlejre som et argument for en aggregeret vinduesfunktion. Det er den indlejrede rækkenummerfunktion og den indlejrede værdi_af udtryk ved rækkefunktionen.
Indlejret rækkenummerfunktion
Funktionen indlejret rækkenummer giver dig mulighed for at henvise til rækkenummeret af strategiske markører i vindueselementer. Her er syntaksen for funktionen:
De rækkemarkører, du kan angive, er:
- BEGIN_PARTITION
- END_PARTITION
- BEGIN_FRAME
- END_FRAME
- CURRENT_ROW
- FRAME_ROW
De første fire markører er selvforklarende. Som for de sidste to repræsenterer CURRENT_ROW-markøren den aktuelle ydre række, og FRAME_ROW repræsenterer den aktuelle indre rammerække.
Som et eksempel på brug af den indlejrede rækkenummerfunktion kan du overveje følgende opgave. Du skal forespørge på visningen Sales.OrderValues og returnere nogle af dens attributter for hver ordre, såvel som forskellen mellem den aktuelle ordreværdi og kundegennemsnittet, men ekskluderer den første og sidste kundeordre fra gennemsnittet.
Denne opgave kan udføres uden indlejrede vinduesfunktioner, men løsningen involverer en del trin:
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;
Her er outputtet af denne forespørgsel, vist her i forkortet form:
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 ...
Ved at bruge indlejrede rækkenummerfunktioner kan opgaven udføres med en enkelt forespørgsel, som sådan:
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;
Desuden kræver den aktuelt understøttede løsning mindst én sortering i planen og flere overførsler af dataene. Løsningen, der anvender indlejrede rækkenummerfunktioner, har alt potentiale til at blive optimeret med afhængighed af indeksrækkefølge og et reduceret antal gennemgange af data. Dette er selvfølgelig implementeringsafhængigt.
Indlejret værdi_af udtryk ved rækkefunktion
Funktionen indlejret værdi_af udtryk ved række gør dig i stand til at interagere med en værdi af et udtryk ved de samme strategiske rækkemarkører, der er nævnt tidligere i et argument for en aggregeret vinduesfunktion. Her er syntaksen for denne funktion:
>) OVER(
Som du kan se, kan du angive et vist negativt eller positivt delta i forhold til rækkemarkøren og eventuelt angive en standardværdi, hvis en række ikke eksisterer på den angivne position.
Denne funktion giver dig en masse kraft, når du skal interagere med forskellige punkter i vindueselementer. Overvej det faktum, at lige så kraftfulde som vinduesfunktioner kan sammenlignes med alternative værktøjer som underforespørgsler, er det, som vinduesfunktioner ikke understøtter, et grundlæggende koncept for en korrelation. Ved at bruge CURRENT_ROW-markøren får du adgang til den ydre række, og på denne måde emulerer korrelationer. Samtidig kan du drage fordel af alle de fordele, som vinduesfunktioner har sammenlignet med underforespørgsler.
Antag for eksempel, at du skal forespørge på visningen Sales.OrderValues og returnere nogle af dens attributter for hver ordre, såvel som forskellen mellem den aktuelle ordreværdi og kundegennemsnittet, men ekskluderer ordrer afgivet på samme dato som den aktuelle ordredato. Dette kræver en evne svarende til en korrelation. Med den indlejrede værdi_af udtryk ved række-funktionen, ved hjælp af CURRENT_ROW-markøren, kan dette nemt opnås som sådan:
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;
Denne forespørgsel skal generere følgende output:
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 ...
Hvis du tænker, at denne opgave lige så let kan opnås med korrelerede underforespørgsler, har du i dette forenklede tilfælde ret. Det samme kan opnås med følgende forespørgsel:
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;
Husk dog, at en underforespørgsel opererer på en uafhængig visning af dataene, hvorimod en vinduesfunktion fungerer på det sæt, der leveres som input til det logiske forespørgselsbehandlingstrin, der håndterer SELECT-sætningen. Normalt har den underliggende forespørgsel ekstra logik som joinforbindelser, filtre, gruppering og sådan. Med underforespørgsler skal du enten forberede en foreløbig CTE eller gentage logikken i den underliggende forespørgsel også i underforespørgslen. Med vinduesfunktioner er der ingen grund til at gentage logikken.
Sig f.eks., at du kun skulle operere på afsendte ordrer (hvor afsendelsesdatoen ikke er NULL), som blev håndteret af medarbejder 3. Løsningen med vinduesfunktionen skal kun tilføje filterprædikaterne én gang, f.eks.:
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;
Denne forespørgsel skal generere følgende output:
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 ...
Løsningen med underforespørgslen skal tilføje filterprædikaterne to gange - én gang i den ydre forespørgsel og én gang i underforespørgslen - som sådan:
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;
Det er enten dette eller tilføjelse af en foreløbig CTE, der tager sig af al filtreringen og enhver anden logik. Uanset hvad du ser på det, med underforespørgsler er der flere kompleksitetslag involveret.
Den anden fordel ved indlejrede vinduesfunktioner er, at hvis vi havde understøttelse af dem i T-SQL, ville det have været let at efterligne den manglende fulde understøttelse af RANGE vinduesrammeenheden. RANGE-indstillingen formodes at give dig mulighed for at definere dynamiske rammer, der er baseret på en offset fra bestillingsværdien i den aktuelle række. Antag for eksempel, at du skal beregne for hver kundeordre fra Sales.OrderValues viser den glidende gennemsnitsværdi for de sidste 14 dage. I henhold til SQL-standarden kan du opnå dette ved at bruge RANGE-indstillingen og INTERVAL-typen, som sådan:
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;
Denne forespørgsel skal generere følgende output:
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 ...
På datoen for denne skrivning er denne syntaks ikke understøttet i T-SQL. Hvis vi havde understøttelse af indlejrede vinduesfunktioner i T-SQL, ville du have været i stand til at emulere denne forespørgsel med følgende kode:
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;
Hvad kan man ikke lide?
Afgiv din stemme
Standard indlejrede vinduesfunktioner virker som et meget kraftfuldt koncept, der muliggør en masse fleksibilitet i interaktion med forskellige punkter i vindueselementer. Jeg er ret overrasket over, at jeg ikke kan finde nogen dækning af konceptet andet end i selve standarden, og at jeg ikke kan se mange platforme implementere det. Forhåbentlig vil denne artikel øge bevidstheden om denne funktion. Hvis du føler, at det kunne være nyttigt for dig at have det tilgængeligt i T-SQL, så sørg for at afgive din stemme!