Norma ISO/IEC 9075:2016 (SQL:2016) definiuje funkcję zwaną zagnieżdżonymi funkcjami okna. Ta funkcja umożliwia zagnieżdżanie dwóch rodzajów funkcji okna jako argumentu funkcji agregującej okna. Pomysł polega na umożliwieniu odwoływania się do numeru wiersza lub wartości wyrażenia w strategicznych znacznikach w elementach okienek. Znaczniki zapewniają dostęp do pierwszego lub ostatniego wiersza w partycji, pierwszego lub ostatniego wiersza w ramce, bieżącego wiersza zewnętrznego i bieżącego wiersza ramki. Ta idea jest bardzo potężna, umożliwiając zastosowanie filtrowania i innych rodzajów manipulacji w funkcji okna, które czasami są trudne do osiągnięcia w inny sposób. Możesz także użyć zagnieżdżonych funkcji okna, aby łatwo emulować inne funkcje, takie jak ramki oparte na ZAKRESIE. Ta funkcja jest obecnie niedostępna w T-SQL. Zamieściłem sugestię ulepszenia SQL Server poprzez dodanie obsługi zagnieżdżonych funkcji okna. Pamiętaj, aby oddać swój głos, jeśli uważasz, że ta funkcja może być dla Ciebie korzystna.
Czego nie dotyczą zagnieżdżone funkcje okien
W chwili pisania tego tekstu nie ma zbyt wielu informacji na temat prawdziwych standardowych funkcji zagnieżdżonych okien. Trudniejsze jest to, że nie znam jeszcze żadnej platformy, która zaimplementowała tę funkcję. W rzeczywistości uruchomienie wyszukiwania w sieci WWW dla zagnieżdżonych funkcji okien zwraca głównie pokrycie i dyskusje na temat zagnieżdżania zgrupowanych funkcji agregujących w oknach funkcji agregujących. Załóżmy na przykład, że chcesz wysłać zapytanie do widoku Sales.OrderValues w przykładowej bazie danych TSQLV5 i zwrócić dla każdego klienta i daty zamówienia, dzienną sumę wartości zamówienia oraz sumę bieżącą do bieżącego dnia. Takie zadanie obejmuje zarówno grupowanie, jak i okienkowanie. Grupujesz wiersze według identyfikatora klienta i daty zamówienia, a następnie dodajesz sumę bieżącą do sumy grupowej wartości zamówienia, na przykład:
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;
To zapytanie generuje następujące dane wyjściowe, pokazane tutaj w skróconej formie:
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 ...
Mimo że ta technika jest całkiem fajna i chociaż wyszukiwanie w Internecie zagnieżdżonych funkcji okien zwraca głównie takie techniki, standard SQL nie ma na myśli tego, co oznacza zagnieżdżone funkcje okien. Ponieważ nie mogłem znaleźć żadnych informacji na ten temat, po prostu musiałem to rozgryźć z samego standardu. Mamy nadzieję, że ten artykuł zwiększy świadomość prawdziwej funkcji zagnieżdżonych funkcji okien i spowoduje, że ludzie zwrócą się do firmy Microsoft i poproszą o dodanie obsługi w SQL Server.
O czym są zagnieżdżone funkcje okien
Zagnieżdżone funkcje okna obejmują dwie funkcje, które można zagnieździć jako argument funkcji agregującej okna. Są to zagnieżdżona funkcja numeru wiersza i zagnieżdżona funkcja wartość_wyrażenia w funkcji wiersza.
Zagnieżdżona funkcja numeru wiersza
Zagnieżdżona funkcja numeru wiersza umożliwia odwołanie się do numeru wiersza znaczników strategicznych w elementach okienek. Oto składnia funkcji:
Znaczniki wierszy, które możesz określić to:
- BEGIN_PARTITION
- END_PARTITION
- BEGIN_RAMKA
- END_FRAME
- CURRENT_ROW
- FRAME_ROW
Pierwsze cztery znaczniki są oczywiste. Jeśli chodzi o ostatnie dwa, znacznik CURRENT_ROW reprezentuje bieżący wiersz zewnętrzny, a FRAME_ROW reprezentuje bieżący wiersz ramki wewnętrznej.
Jako przykład użycia zagnieżdżonej funkcji numeru wiersza rozważ następujące zadanie. Musisz wysłać zapytanie do widoku Sales.OrderValues i zwrócić dla każdego zamówienia niektóre jego atrybuty, a także różnicę między bieżącą wartością zamówienia a średnią klienta, ale z wyłączeniem pierwszego i ostatniego zamówienia klienta ze średniej.
To zadanie można osiągnąć bez zagnieżdżonych funkcji okien, ale rozwiązanie obejmuje kilka kroków:
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;
Oto wynik tego zapytania, pokazany tutaj w skróconej formie:
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 ...
Używając zagnieżdżonych funkcji numerów wierszy, zadanie jest osiągalne za pomocą jednego zapytania, na przykład:
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;
Ponadto obecnie obsługiwane rozwiązanie wymaga co najmniej jednego sortowania w planie i wielokrotnego przekazywania danych. Rozwiązanie wykorzystujące zagnieżdżone funkcje numerów wierszy ma cały potencjał optymalizacji w oparciu o kolejność indeksów i zmniejszoną liczbę przejść przez dane. To oczywiście zależy od implementacji.
Zagnieżdżona wartość_wyrażenia w funkcji wiersza
Zagnieżdżona funkcja wartość_wyrażenia w wierszu umożliwia interakcję z wartością wyrażenia w tych samych strategicznych znacznikach wierszy, o których wspomniano wcześniej w argumencie funkcji agregującej okna. Oto składnia tej funkcji:
>) OVER(
Jak widać, możesz określić pewną ujemną lub dodatnią deltę w odniesieniu do znacznika wiersza i opcjonalnie podać wartość domyślną w przypadku, gdy wiersz nie istnieje w określonej pozycji.
Ta funkcja zapewnia dużą moc, gdy zachodzi potrzeba interakcji z różnymi punktami elementów okienek. Rozważ fakt, że tak potężne, jak funkcje okien mogą być porównywane do alternatywnych narzędzi, takich jak podzapytania, to to, czego funkcje okien nie obsługują, jest podstawową koncepcją korelacji. Używając znacznika CURRENT_ROW uzyskujesz dostęp do zewnętrznego wiersza iw ten sposób emulujesz korelacje. Jednocześnie możesz korzystać ze wszystkich zalet funkcji okien w porównaniu z podzapytaniami.
Na przykład załóżmy, że trzeba wykonać zapytanie do widoku Sales.OrderValues i zwrócić dla każdego zamówienia niektóre jego atrybuty, a także różnicę między bieżącą wartością zamówienia a średnią klienta, ale z wyłączeniem zamówień złożonych w tym samym dniu co aktualna data zamówienia. Wymaga to zdolności podobnej do korelacji. Dzięki zagnieżdżonej funkcji value_of expression at row, używając znacznika CURRENT_ROW, można to łatwo osiągnąć, tak jak:
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;
To zapytanie ma wygenerować następujący wynik:
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 ...
Jeśli myślisz, że to zadanie można równie łatwo wykonać za pomocą skorelowanych podzapytań, w tym uproszczonym przypadku miałbyś rację. To samo można osiągnąć za pomocą następującego zapytania:
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;
Należy jednak pamiętać, że podzapytanie działa na niezależnym widoku danych, podczas gdy funkcja okna działa na zestawie, który jest dostarczany jako dane wejściowe do logicznego kroku przetwarzania zapytania, który obsługuje klauzulę SELECT. Zwykle podstawowe zapytanie ma dodatkową logikę, taką jak sprzężenia, filtry, grupowanie i tym podobne. W przypadku podzapytań musisz albo przygotować wstępne CTE, albo powtórzyć logikę podstawowego zapytania również w podzapytaniu. Dzięki funkcjom okien nie ma potrzeby powtarzania żadnej logiki.
Załóżmy na przykład, że miałeś operować tylko na wysłanych zamówieniach (gdzie data wysyłki nie jest NULL), które były obsługiwane przez pracownika 3. Rozwiązanie z funkcją okna wymaga dodania predykatów filtrujących tylko raz, tak jak:
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;
To zapytanie ma wygenerować następujący wynik:
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 ...
Rozwiązanie z podzapytaniem wymaga dwukrotnego dodania predykatów filtrujących — raz w zapytaniu zewnętrznym i raz w podzapytaniu — w ten sposób:
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;
Jest to albo to, albo dodanie wstępnego CTE, które zajmuje się całym filtrowaniem i wszelką inną logiką. W każdym razie, gdy na to spojrzysz, w przypadku podzapytań jest zaangażowanych więcej warstw złożoności.
Inną zaletą zagnieżdżonych funkcji okien jest to, że gdybyśmy mieli wsparcie dla tych w T-SQL, łatwo byłoby emulować brakujące pełne wsparcie dla jednostki ramy okna RANGE. Opcja RANGE ma umożliwić zdefiniowanie ramek dynamicznych, które bazują na przesunięciu względem wartości porządkowania w bieżącym wierszu. Załóżmy na przykład, że musisz obliczyć dla każdego zamówienia klienta z Sales.OrderValues wyświetlić średnią ruchomą wartość z ostatnich 14 dni. Zgodnie ze standardem SQL można to osiągnąć za pomocą opcji RANGE i typu INTERVAL, na przykład:
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;
To zapytanie ma wygenerować następujący wynik:
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 ...
W chwili pisania tego tekstu ta składnia nie jest obsługiwana w języku T-SQL. Gdybyśmy mieli obsługę zagnieżdżonych funkcji okien w T-SQL, bylibyśmy w stanie emulować to zapytanie za pomocą następującego kodu:
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;
Czego nie lubić?
Oddaj swój głos
Standardowe zagnieżdżone funkcje okien wydają się być bardzo potężną koncepcją, która zapewnia dużą elastyczność w interakcji z różnymi punktami elementów okienek. Jestem dość zaskoczony, że nie mogę znaleźć żadnego opisu koncepcji poza samym standardem i że nie widzę wielu platform, które go implementują. Mam nadzieję, że ten artykuł zwiększy świadomość tej funkcji. Jeśli uważasz, że przydatne byłoby udostępnienie go w T-SQL, oddaj swój głos!