ISO/IEC 9075:2016-standarden (SQL:2016) definierar en funktion som kallas kapslade fönsterfunktioner. Den här funktionen låter dig kapsla två typer av fönsterfunktioner som ett argument för en fönsteraggregatfunktion. Tanken är att låta dig referera till antingen ett radnummer, eller till ett värde på ett uttryck, vid strategiska markörer i fönsterelement. Markörerna ger dig tillgång till den första eller sista raden i partitionen, den första eller sista raden i ramen, den aktuella yttre raden och den aktuella ramraden. Denna idé är mycket kraftfull, vilket gör att du kan tillämpa filtrering och andra typer av manipulationer inom din fönsterfunktion som ibland är svåra att åstadkomma annars. Du kan också använda kapslade fönsterfunktioner för att enkelt emulera andra funktioner, som RANGE-baserade ramar. Den här funktionen är för närvarande inte tillgänglig i T-SQL. Jag postade ett förslag för att förbättra SQL Server genom att lägga till stöd för kapslade fönsterfunktioner. Se till att lägga till din röst om du känner att den här funktionen kan vara till nytta för dig.
Vad handlar inte kapslade fönsterfunktioner om
När detta skrivs finns det inte mycket information tillgänglig där ute om de verkliga standardkapslade fönsterfunktionerna. Det som gör det svårare är att jag inte känner till någon plattform som implementerat den här funktionen ännu. Att köra en webbsökning efter kapslade fönsterfunktioner returnerar i själva verket mestadels täckning av och diskussioner om kapsling av grupperade aggregerade funktioner inom fönsterbaserade aggregerade funktioner. Anta till exempel att du vill fråga vyn Sales.OrderValues i TSQLV5-exempeldatabasen och returnera för varje kund och orderdatum, den dagliga summan av ordervärdena och den löpande summan fram till den aktuella dagen. En sådan uppgift innebär både gruppering och fönster. Du grupperar raderna efter kund-ID och beställningsdatum och lägger en löpande summa ovanpå gruppsumman av beställningsvärdena, så här:
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;
Den här frågan genererar följande utdata, som visas här i förkortad 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 ...
Även om den här tekniken är ganska cool, och även om webbsökningar efter kapslade fönsterfunktioner huvudsakligen returnerar sådana tekniker, är det inte vad SQL-standarden menar med kapslade fönsterfunktioner. Eftersom jag inte kunde hitta någon information där ute om ämnet, var jag bara tvungen att ta reda på det från själva standarden. Förhoppningsvis kommer den här artikeln att öka medvetenheten om funktionen för verkliga kapslade fönsterfunktioner och få människor att vända sig till Microsoft och be om att få lägga till stöd för det i SQL Server.
Vad handlar kapslade fönsterfunktioner om
Kapslade fönsterfunktioner inkluderar två funktioner som du kan kapsla som ett argument för en aggregerad fönsterfunktion. Dessa är den kapslade radnummerfunktionen och det kapslade värdet_av uttryck vid radfunktionen.
Inkapslad radnummerfunktion
Den kapslade radnummerfunktionen låter dig referera till radnumret med strategiska markörer i fönsterelement. Här är syntaxen för funktionen:
Radmarkörerna som du kan ange är:
- BEGIN_PARTITION
- END_PARTITION
- BEGIN_FRAME
- END_FRAME
- CURRENT_ROW
- FRAME_ROW
De fyra första markörerna är självförklarande. När det gäller de två sista representerar CURRENT_ROW-markören den aktuella yttre raden, och FRAME_ROW representerar den aktuella inre ramraden.
Som ett exempel för att använda den kapslade radnummerfunktionen, överväg följande uppgift. Du måste fråga i vyn Sales.OrderValues och returnera några av dess attribut för varje beställning, såväl som skillnaden mellan det aktuella beställningsvärdet och kundgenomsnittet, men exklusive den första och sista kundordern från genomsnittet.
Denna uppgift kan utföras utan kapslade fönsterfunktioner, men lösningen innefattar en hel del steg:
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;
Här är resultatet av denna fråga, som visas här i förkortad 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 ...
Genom att använda kapslade radnummerfunktioner kan uppgiften uppnås med en enda fråga, som så:
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;
Dessutom kräver den för närvarande stödda lösningen minst en sortering i planen och flera överföringar av data. Lösningen som använder kapslade radnummerfunktioner har all potential att bli optimerad med beroende av indexordning och ett minskat antal övergångar över data. Detta är naturligtvis implementeringsberoende dock.
Inkapslat värde_av uttryck vid radfunktion
Funktionen kapslad värde_av uttryck vid rad gör att du kan interagera med ett värde för ett uttryck vid samma strategiska radmarkörer som nämnts tidigare i ett argument för en aggregerad fönsterfunktion. Här är syntaxen för denna funktion:
>) ÖVER(
Som du kan se kan du ange ett visst negativt eller positivt delta med avseende på radmarkören, och valfritt ange ett standardvärde om en rad inte finns på den angivna positionen.
Denna förmåga ger dig mycket kraft när du behöver interagera med olika punkter i fönsterelement. Tänk på det faktum att lika kraftfulla som fönsterfunktioner kan jämföras med alternativa verktyg som subqueries, vad fönsterfunktioner inte stöder är ett grundläggande koncept för en korrelation. Genom att använda CURRENT_ROW-markören får du tillgång till den yttre raden, och på så sätt emulerar korrelationer. Samtidigt får du dra nytta av alla fördelar som fönsterfunktioner har jämfört med underfrågor.
Anta som ett exempel att du behöver fråga i vyn Sales.OrderValues och returnera några av dess attribut för varje beställning, såväl som skillnaden mellan det aktuella beställningsvärdet och kundgenomsnittet, men exklusive beställningar som görs samma datum som det aktuella beställningsdatumet. Detta kräver en förmåga som liknar en korrelation. Med funktionen för kapslade uttrycksvärde vid rad, med CURRENT_ROW-markören, kan detta enkelt uppnås så här:
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;
Den här frågan är tänkt att generera följande utdata:
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 ...
Om du tror att den här uppgiften är lika lätt att uppnå med korrelerade underfrågor, i detta förenklade fall skulle du ha rätt. Detsamma kan uppnås med följande fråga:
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;
Kom dock ihåg att en underfråga arbetar på en oberoende vy av data, medan en fönsterfunktion fungerar på uppsättningen som tillhandahålls som input till det logiska frågebearbetningssteget som hanterar SELECT-satsen. Vanligtvis har den underliggande frågan extra logik som kopplingar, filter, gruppering och sådant. Med delfrågor måste du antingen förbereda en preliminär CTE eller upprepa logiken i den underliggande frågan även i underfrågan. Med fönsterfunktioner behöver du inte upprepa någon av logiken.
Säg till exempel att du bara skulle arbeta på skickade beställningar (där leveransdatumet inte är NULL) som hanterades av anställd 3. Lösningen med fönsterfunktionen behöver bara lägga till filterpredikaten en gång, som så:
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;
Den här frågan är tänkt att generera följande utdata:
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 underfrågan måste lägga till filterpredikaten två gånger - en gång i den yttre frågan och en gång i underfrågan - som så:
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 är antingen detta eller att lägga till en preliminär CTE som tar hand om all filtrering och all annan logik. Hur som helst du tittar på det, med underfrågor är det fler komplexitetslager inblandade.
Den andra fördelen med kapslade fönsterfunktioner är att om vi hade stöd för de i T-SQL, skulle det ha varit lätt att emulera det saknade fullständiga stödet för RANGE-fönsterramsenheten. Alternativet RANGE är tänkt att göra det möjligt för dig att definiera dynamiska ramar som är baserade på en offset från beställningsvärdet i den aktuella raden. Anta till exempel att du behöver beräkna för varje kundorder från Sales.OrderValues visar det glidande medelvärdet för de senaste 14 dagarna. Enligt SQL-standarden kan du uppnå detta med RANGE-alternativet och INTERVAL-typen, så här:
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;
Den här frågan är tänkt att generera följande utdata:
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 ...
När detta skrivs stöds inte denna syntax i T-SQL. Om vi hade stöd för kapslade fönsterfunktioner i T-SQL, skulle du ha kunnat emulera den här frågan med följande kod:
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;
Vad ska man inte gilla?
Lägg din röst
De vanliga kapslade fönsterfunktionerna verkar vara ett mycket kraftfullt koncept som möjliggör mycket flexibilitet i interaktion med olika punkter i fönsterelement. Jag är ganska förvånad över att jag inte kan hitta någon täckning av konceptet annat än i själva standarden, och att jag inte ser många plattformar som implementerar det. Förhoppningsvis kommer den här artikeln att öka medvetenheten om den här funktionen. Om du känner att det kan vara användbart för dig att ha det tillgängligt i T-SQL, se till att lägga din röst!