Ten artykuł jest czwartą częścią serii poświęconej błędom, pułapkom i najlepszym praktykom w T-SQL. Wcześniej zajmowałem się determinizmem, podzapytaniami i złączeniami. W tym miesiącu artykuł skupia się na błędach, pułapkach i najlepszych praktykach związanych z funkcjami okien. Dziękujemy Erlandowi Sommarskogowi, Aaronowi Bertrandowi, Alejandro Mesa, Umachandarowi Jayachandranowi (UC), Fabiano Neves Amorim, Milosowi Radivojevicowi, Simonowi Sabinowi, Adamowi Machanicowi, Thomasowi Grohserowi, Chan Ming Manowi i Paulowi White'owi za przedstawienie swoich pomysłów!
W moich przykładach użyję przykładowej bazy danych o nazwie TSQLV5. Skrypt, który tworzy i wypełnia tę bazę danych, oraz jego diagram ER można znaleźć tutaj.
Istnieją dwie typowe pułapki związane z funkcjami okna, z których obie są wynikiem sprzecznych z intuicją domyślnych wartości domyślnych narzuconych przez standard SQL. Jedna pułapka dotyczy obliczeń sum bieżących, w których otrzymujesz ramkę okna z niejawną opcją RANGE. Inna pułapka jest nieco powiązana, ale ma poważniejsze konsekwencje, obejmując niejawną definicję ramki dla funkcji FIRST_VALUE i LAST_VALUE.
Ramka okna z niejawną opcją RANGE
Nasza pierwsza pułapka polega na obliczeniu bieżących sum przy użyciu funkcji agregacji okna, w której jawnie określasz klauzulę kolejności okien, ale nie określasz jawnie jednostki ramy okna (ROWS lub RANGE) i związanego z nią zasięgu ramki okna, np. ROWS NIEOGRANICZONE POPRZEDNIE. Ukryte niewykonanie zobowiązania jest sprzeczne z intuicją, a jego konsekwencje mogą być zaskakujące i bolesne.
Aby zademonstrować tę pułapkę, użyję tabeli o nazwie Transakcje zawierające dwa miliony transakcji na koncie bankowym z kredytami (wartości dodatnie) i debetami (wartości ujemne). Uruchom następujący kod, aby utworzyć tabelę Transakcje i wypełnij ją przykładowymi danymi:
WŁĄCZ NR LICZNIKA; UŻYJ TSQLV5; -- http://tsql.solidq.com/SampleDatabases/TSQLV5.zip DROP TABELA JEŚLI ISTNIEJE dbo.Transactions; CREATE TABLE dbo.Transactions ( actid INT NOT NULL, tranid INT NOT NULL, wartość MONEY NOT NULL, CONSTRAINT PK_Transactions PRIMARY KEY(actid, tranid) -- tworzy indeks POC ); DECLARE @num_partitions AS INT =100, @rows_per_partition AS INT =20000; INSERT INTO dbo.Transactions WITH (TABLOCK) (actid, tranid, val) SELECT NP.n, RPP.n, (ABS(CHECKSUM(NEWID())%2)*2-1) * (1 + ABS(CHECKSUM() NEWID())%5)) FROM dbo.GetNums(1, @num_partitions) AS NP CROSS JOIN dbo.GetNums(1, @rows_per_partition) AS RPP;
Nasza pułapka ma zarówno logiczną stronę z potencjalnym logicznym błędem, jak i stronę wydajności z obniżeniem wydajności. Spadek wydajności ma znaczenie tylko wtedy, gdy funkcja okna jest zoptymalizowana za pomocą operatorów przetwarzania w trybie wiersza. W SQL Server 2016 wprowadzono operator Window Aggregate w trybie wsadowym, który usuwa część pułapki związanej z obniżeniem wydajności, ale przed SQL Server 2019 ten operator jest używany tylko wtedy, gdy w danych znajduje się indeks magazynu kolumn. SQL Server 2019 wprowadza tryb wsadowy obsługi magazynu wierszy, dzięki czemu można uzyskać przetwarzanie w trybie wsadowym, nawet jeśli w danych nie ma indeksów magazynu kolumn. Aby zademonstrować karę za wydajność z przetwarzaniem w trybie wiersza, jeśli uruchamiasz próbki kodu w tym artykule w programie SQL Server 2019 lub nowszym albo w Azure SQL Database, użyj następującego kodu, aby ustawić poziom zgodności bazy danych na 140, tak aby nie włączać jeszcze trybu wsadowego w sklepie wierszowym:
ALTER DATABASE TSQLV5 SET COMPATIBILITY_LEVEL =140;
Użyj następującego kodu, aby włączyć statystyki czasu i I/O w sesji:
USTAW CZAS STATYSTYK, IO WŁĄCZONE;
Aby uniknąć czekania na wydrukowanie dwóch milionów wierszy w SSMS, sugeruję uruchomienie przykładów kodu w tej sekcji z włączoną opcją Odrzuć wyniki po wykonaniu (przejdź do Opcje zapytania, Wyniki, Siatka i zaznacz Odrzuć wyniki po wykonaniu).
Zanim dojdziemy do pułapki, rozważmy następujące zapytanie (nazwijmy je Zapytanie 1), które oblicza saldo konta bankowego po każdej transakcji, stosując sumę bieżącą przy użyciu funkcji agregującej okna z wyraźną specyfikacją ramki:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid WIERSZE NIEOGRANICZONE POSTĘPOWANIE ) AS balance FROM dbo.Transactions;
Plan dla tego zapytania, wykorzystujący przetwarzanie w trybie wierszowym, pokazano na rysunku 1.
Rysunek 1:Plan dla zapytania 1, przetwarzanie w trybie wierszowym
Plan pobiera dane zamówione w przedsprzedaży z indeksu klastrowego tabeli. Następnie używa operatorów segmentu i projektu sekwencji do obliczenia numerów wierszy, aby dowiedzieć się, które wiersze należą do ramki bieżącego wiersza. Następnie używa operatorów Segment, Window Spool i Stream Aggregate do obliczenia funkcji agregującej okna. Operator Window Spool służy do buforowania wierszy ramek, które następnie muszą zostać zagregowane. Bez żadnej specjalnej optymalizacji plan musiałby zapisywać dla każdego wiersza wszystkie odpowiednie wiersze ramek do bufora, a następnie je agregować. Spowodowałoby to złożoność kwadratową lub N. Dobrą wiadomością jest to, że kiedy klatka zaczyna się od UNBOUNDED PRECEDING, SQL Server identyfikuje przypadek jako szybka ścieżka przypadku, w którym po prostu bierze sumę bieżącą z poprzedniego wiersza i dodaje wartość bieżącego wiersza, aby obliczyć sumę bieżącą bieżącego wiersza, co skutkuje skalowaniem liniowym. W tym trybie szybkiej ścieżki plan zapisuje do szpuli tylko dwa wiersze na wiersz wejściowy — jeden z agregatem, a drugi ze szczegółami.
Window Spool może być fizycznie zaimplementowany na jeden z dwóch sposobów. Albo jako szybki bufor w pamięci, który został specjalnie zaprojektowany dla funkcji okna, albo jako powolny bufor na dysku, który jest zasadniczo tabelą tymczasową w tempdb. Jeśli liczba wierszy, które należy zapisać w szpuli na wiersz podstawowy może przekroczyć 10 000 lub jeśli program SQL Server nie może przewidzieć liczby, użyje wolniejszego buforowania na dysku. W naszym planie zapytań mamy dokładnie dwa wiersze zapisane w buforze na wiersz bazowy, więc SQL Server używa buforu w pamięci. Niestety, z planu nie sposób odróżnić, jaką szpulę otrzymujesz. Są dwa sposoby, aby to rozgryźć. Jednym z nich jest użycie rozszerzonego zdarzenia o nazwie window_spool_ondisk_warning. Inną opcją jest włączenie STATISTICS IO i sprawdzenie liczby odczytów logicznych raportowanych dla tabeli o nazwie Worktable. Liczba większa niż zero oznacza, że masz szpulę na dysku. Zero oznacza, że masz szpulę pamięci. Oto statystyki I/O dla naszego zapytania:
Logiczne odczyty tabeli „Worktable”:0. Logiczne odczyty tabeli „Transactions”:6208.Jak widać, wykorzystaliśmy szpulę pamięci. Zwykle dzieje się tak, gdy używasz jednostki ramy okiennej ROWS z NIEOGRANICZONYM POSTĘPOWANIEM jako pierwszym ogranicznikiem.
Oto statystyki czasu dla naszego zapytania:
Czas procesora:4297 ms, czas, który upłynął:4441 ms.Wykonanie tego zapytania na moim komputerze zajęło około 4,5 sekundy, a wyniki zostały odrzucone.
Teraz do połowu. Jeśli użyjesz opcji RANGE zamiast ROWS, z tymi samymi ogranicznikami, może wystąpić subtelna różnica w znaczeniu, ale duża różnica w wydajności w trybie wierszowym. Różnica w znaczeniu jest istotna tylko wtedy, gdy nie masz całkowitego zamówienia, tj. jeśli zamawiasz coś, co nie jest unikalne. Opcja ROWS UNBOUNDED PRECEDING zatrzymuje się na bieżącym wierszu, więc w przypadku remisów obliczenia są niedeterministyczne. I odwrotnie, opcja RANGE UNBOUNDED PRECEDING wybiega przed bieżący wiersz i uwzględnia remisy, jeśli są obecne. Wykorzystuje podobną logikę do opcji TOP WITH TIES. Kiedy masz całkowite uporządkowanie, tj. zamawiasz według czegoś wyjątkowego, nie ma żadnych powiązań do uwzględnienia, dlatego WIERSZE i ZAKRES stają się w takim przypadku logicznie równoważne. Problem polega na tym, że kiedy używasz RANGE, SQL Server zawsze używa buforu na dysku w trybie przetwarzania w trybie wiersza, ponieważ podczas przetwarzania danego wiersza nie można przewidzieć, o ile więcej wierszy zostanie uwzględnionych. Może to mieć poważny spadek wydajności.
Rozważ następujące zapytanie (nazwij je Zapytanie 2), które jest takie samo jak Zapytanie 1, używając tylko opcji RANGE zamiast ROWS:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ZAKRES NIEOGRANICZONY PRECEDING ) AS saldo FROM dbo.Transactions;
Plan dla tego zapytania pokazano na rysunku 2.
Rysunek 2:Plan dla zapytania 2, przetwarzanie w trybie wierszowym
Zapytanie 2 jest logicznie równoważne z Zapytaniem 1, ponieważ mamy porządek całkowity; jednak ponieważ używa RANGE, jest zoptymalizowany za pomocą buforowania na dysku. Zauważ, że w planie dla Zapytania 2 bufor okna wygląda tak samo jak w planie dla Zapytania 1, a szacunkowe koszty są takie same.
Oto czas i statystyki I/O dla wykonania Zapytania 2:
Czas procesora:19515 ms, czas, który upłynął:20201 ms.Odczyty logiczne tabeli „Worktable”:12044701. Logiczne odczyty tabeli „Transactions”:6208.
Zwróć uwagę na dużą liczbę logicznych odczytów względem Worktable, co wskazuje, że masz bufor na dysku. Czas działania jest ponad cztery razy dłuższy niż w przypadku Zapytania 1.
Jeśli myślisz, że jeśli tak jest, po prostu unikniesz korzystania z opcji RANGE, chyba że naprawdę musisz uwzględnić remisy, to jest to dobre myślenie. Problem polega na tym, że jeśli używasz funkcji okna, która obsługuje ramkę (agregaty, FIRST_VALUE, LAST_VALUE) z jawną klauzulą kolejności okien, ale bez wzmianki o jednostce ramki okna i jej skojarzonym zakresie, domyślnie otrzymujesz RANGE UNBOUNDED PRECEDING . Ta wartość domyślna jest podyktowana standardem SQL, a standard wybrał ją, ponieważ generalnie preferuje bardziej deterministyczne opcje jako wartości domyślne. Poniższe zapytanie (nazwijmy je Zapytanie 3) jest przykładem, który wpada w tę pułapkę:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ) AS saldo FROM dbo.Transactions;
Często ludzie piszą w ten sposób, zakładając, że domyślnie otrzymują WIERSZE NIEOGRANICZONEGO POSTĘPOWANIA, nie zdając sobie sprawy, że w rzeczywistości otrzymują NIEOGRANICZONE POSTĘPOWANIE ZAKRESU. Chodzi o to, że ponieważ funkcja używa całkowitego porządku, otrzymujesz taki sam wynik, jak w przypadku ROWS, więc nie możesz powiedzieć, że jest problem z wynikiem. Ale wyniki, które uzyskasz, są takie jak dla zapytania 2. Widzę, że ludzie cały czas wpadają w tę pułapkę.
Najlepszym sposobem uniknięcia tego problemu jest użycie funkcji okna z ramą, dokładne określenie jednostki ramy okna i jej zasięgu oraz generalnie preferowanie WIERSZY. Zastrzeż użycie RANGE tylko w przypadkach, w których zamówienie nie jest wyjątkowe i musisz uwzględnić krawaty.
Rozważ następujące zapytanie ilustrujące przypadek, w którym istnieje koncepcyjna różnica między ROWS i RANGE:
SELECT datazamówienia, id zamówienia, wart, SUM(wartość) OVER( ORDER BY datazam /pre>To zapytanie generuje następujące dane wyjściowe:
data zamówienia id zamówienia val sumrows sumrange ---------- -------- -------- -------- -------- - 2017-07-04 10248 440,00 440,00 440,00 2017-07-05 10249 1863,40 2303,40 2303,40 2017-07-08 10250 1552,60 3856,00 4510,06 2017-07-08 10251 654.06 4510.06 4510.06 2017-07-09 10252 3597.90 8107.96 8107.96 ...Zwróć uwagę na różnicę w wynikach dla wierszy, w których ta sama data zamówienia pojawia się więcej niż jeden raz, jak w przypadku 8 lipca 2017 r. Zwróć uwagę, że opcja ROWS nie zawiera powiązań, a zatem jest niedeterministyczna, oraz jak robi to opcja RANGE zawierać powiązania, a zatem jest zawsze deterministyczny.
Jest jednak wątpliwe, czy w praktyce masz przypadki, w których zamawiasz coś, co nie jest unikalne, i naprawdę potrzebujesz włączenia powiązań, aby obliczenia były deterministyczne. W praktyce prawdopodobnie dużo częściej robi się jedną z dwóch rzeczy. Jednym z nich jest zerwanie więzi poprzez dodanie czegoś do porządku okna, aby było unikalne i w ten sposób skutkować deterministycznymi obliczeniami, takimi jak:
SELECT datazamTo zapytanie generuje następujące dane wyjściowe:
data zamówienia id zamówienia val suma bieżąca ---------- -------- --------- ----------- 2017-07-04 10248 440,00 440,00 05.07.2017 10249 1863.40 2303.40 08.07.2017 10250 1552.60 3856.00 2017-07-08 10251 654.06 4510.06 2017-07-09 10252 3597.90 8107.96 ...Inną opcją jest zastosowanie wstępnego grupowania, w naszym przypadku według daty zamówienia, na przykład:
SELECT datazamówienia, SUM(wartość) AS suma dni, SUM(SUM(wartość)) OVER(ORDER BY datazamoTo zapytanie generuje następujące dane wyjściowe, w których data każdego zamówienia pojawia się tylko raz:
data zamówienia suma dzienna bieżąca ---------- --------- ------------ 2017-07-04 440,00 440,00 2017-07-05 1863,40 2303,40 2017-07-08 2206.66 4510.06 2017-07-09 3597.90 8107.96 ... W każdym razie pamiętaj o najlepszych praktykach tutaj!
Dobrą wiadomością jest to, że jeśli korzystasz z SQL Server 2016 lub nowszego i masz indeks magazynu kolumn w danych (nawet jeśli jest to fałszywy filtrowany indeks magazynu kolumn) lub jeśli korzystasz z SQL Server 2019 lub nowszego, lub w Azure SQL Database, niezależnie od obecności indeksów magazynu kolumn, wszystkie trzy wyżej wymienione zapytania są optymalizowane za pomocą operatora Window Aggregate w trybie wsadowym. Dzięki temu operatorowi wiele nieefektywności przetwarzania w trybie wierszowym zostaje wyeliminowanych. Ten operator w ogóle nie używa bufora, więc nie ma problemu z buforowaniem w pamięci w porównaniu do bufora na dysku. Wykorzystuje bardziej wyrafinowane przetwarzanie, w którym może zastosować wiele równoległych przejść przez okno wierszy w pamięci zarówno dla ROWS, jak i RANGE.
Aby zademonstrować korzystanie z optymalizacji w trybie wsadowym, upewnij się, że poziom zgodności Twojej bazy danych jest ustawiony na 150 lub wyższy:
ZMIEŃ BAZĘ DANYCH TSQLV5 USTAW COMPATIBILITY_LEVEL =150;Uruchom ponownie zapytanie 1:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid WIERSZE NIEOGRANICZONE POSTĘPOWANIE ) AS balance FROM dbo.Transactions;Plan dla tego zapytania pokazano na rysunku 3.
Rysunek 3:Plan dla zapytania 1, przetwarzanie w trybie wsadowym
Oto statystyki wydajności, które uzyskałem dla tego zapytania:
Czas procesora:937 ms, czas, który upłynął:983 ms.
Odczyty logiczne tabeli „Transakcje”:6208.Czas działania spadł do 1 sekundy!
Uruchom ponownie Zapytanie 2 z opcją RANGE:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ZAKRES NIEOGRANICZONY PRECEDING ) AS saldo FROM dbo.Transactions;Plan dla tego zapytania pokazano na rysunku 4.
Rysunek 2:Plan dla zapytania 2, przetwarzanie w trybie wsadowym
Oto statystyki wydajności, które uzyskałem dla tego zapytania:
Czas procesora:969 ms, czas, który upłynął:1048 ms.
Odczyty logiczne tabeli „Transakcje”:6208.Wydajność jest taka sama jak dla zapytania 1.
Uruchom ponownie zapytanie 3 z niejawną opcją RANGE:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ) AS saldo FROM dbo.Transactions;Plan i dane dotyczące wydajności są oczywiście takie same jak w przypadku zapytania 2.
Gdy skończysz, uruchom następujący kod, aby wyłączyć statystyki wydajności:
USTAW CZAS STATYSTYK, WYŁĄCZ IO;Nie zapomnij również wyłączyć opcji Odrzuć wyniki po wykonaniu w programie SSMS.
Niejawna ramka z FIRST_VALUE i LAST_VALUE
Funkcje FIRST_VALUE i LAST_VALUE są funkcjami przesunięcia okna, które zwracają wyrażenie odpowiednio z pierwszego lub ostatniego wiersza w ramce okna. Trudne jest to, że często, gdy ludzie używają ich po raz pierwszy, nie zdają sobie sprawy, że obsługują ramkę, a raczej myślą, że dotyczą całej partycji.
Rozważ następującą próbę zwrócenia informacji o zamówieniu oraz wartości pierwszego i ostatniego zamówienia klienta:
SELECT custid, datazamowienia, idzamowienia, wart, FIRST_VALUE(wal) OVER(PARTITION BY custid ORDER BY datazamowienia, idzamowienia) AS firstval, LAST_VALUE(val) OVER(PARTITION BY custid ORDER BY datazamowienia, idzamowienia) AS lastval FROM Sales. OrderValues ORDER BY custid, orderdate, orderid;Jeśli błędnie sądzisz, że te funkcje działają na całej partycji okna, co jest przekonaniem wielu osób, które używają tych funkcji po raz pierwszy, naturalnie oczekujesz, że FIRST_VALUE zwróci wartość zamówienia pierwszego zamówienia klienta, a LAST_VALUE zwróci wartość wartość zamówienia ostatniego zamówienia klienta. W praktyce jednak te funkcje obsługują ramkę. Przypominamy, że w przypadku funkcji obsługujących ramkę, gdy określisz klauzulę kolejności okna, ale nie jednostkę ramy okna i jej zakres, domyślnie otrzymujesz RANGE UNBOUNDED PRECEDING. Dzięki funkcji FIRST_VALUE otrzymasz oczekiwany wynik, ale jeśli Twoje zapytanie zostanie zoptymalizowane za pomocą operatorów trybu wiersza, zapłacisz karę za użycie buforu na dysku. Z funkcją LAST_VALUE jest jeszcze gorzej. Nie tylko zapłacisz karę za buforowanie na dysku, ale zamiast pobierać wartość z ostatniego wiersza w partycji, otrzymasz wartość z bieżącego wiersza!
Oto wynik powyższego zapytania:
identyfikator klienta data zamówienia id zamówienia val pierwszywal ostatni wart ------- ---------- -------- ---------- ------ ---- ---------- 1 2018-08-25 10643 814,50 814,50 814,50 1 2018-10-03 10692 878,00 814,50 878,00 1 2018-10-13 10702 330,00 814,50 330,00 1 2019-01-15 10835 845.80 814.50 845.80 1 2019-03-16 10952 471.20 814.50 471.20 1 2019-04-09 11011 933.50 814.50 933.50 2 2017-09-18 10308 88.80 88.80 88.80 2 2018-08-08 10625 479.75 88.80 479.75 2 2018-11-28 10759 320,00 88,80 320,00 2 2019-03-04 10926 514,40 88,80 514,40 3 2017-11-27 10365 403.20 403.20 403.20 3 2018-04-15 10507 749.06 403.20 749.06 3 2018-05-13 10535 1940.85 403.20 1940.85 3 2018-06-19 10573 2082,00 403,20 2082,00 3 2018-09-22 10677 813,37 403,20 813,37 3 2018-09-25 10682 375,50 403,20 375,50 3 2019-01-28 10856 660,00 403,20 660,00 ...Często, gdy ludzie widzą takie wyjście po raz pierwszy, myślą, że SQL Server ma błąd. Ale oczywiście tak nie jest; to po prostu domyślny standard SQL. W zapytaniu jest błąd. Zdając sobie sprawę, że w grę wchodzi ramka, chcesz wyraźnie określić specyfikację ramki i użyć minimalnej klatki, która przechwytuje wiersz, którego szukasz. Upewnij się również, że używasz jednostki ROWS. Aby uzyskać pierwszy wiersz z partycji, użyj funkcji FIRST_VALUE z ramką ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Aby uzyskać ostatni wiersz z partycji, użyj funkcji LAST_VALUE z ramką ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING.
Oto nasze poprawione zapytanie z naprawionym błędem:
SELECT custid, datazamówienia, id zamówienia, val, FIRST_VALUE(val) OVER( PARTITION BY custid ORDER BY datazamowienia, id zamówienia ROWS POMIĘDZY BEZ OGRANICZENIA POTRZEBUJĄCEGO I BIEŻĄCEGO ROW ) AS firstval, LAST_VALUE(val) OVER( PARTITION BY custid, ORDER BY orderid WIERSZE POMIĘDZY BIEŻĄCYM WIERSZEM A NIEOGRANICZONYMI NASTĘPUJĄCYMI ) AS lastval FROM Sales.OrderValues ORDER BY custid, orderdate, orderid;Tym razem otrzymasz prawidłowy wynik:
identyfikator klienta data zamówienia id zamówienia val pierwszywal ostatni wart ------- ---------- -------- ---------- ------ ---- ---------- 1 2018-08-25 10643 814,50 814,50 933,50 1 2018-10-03 10692 878,00 814,50 933,50 1 2018-10-13 10702 330,00 814,50 933,50 1 2019-01-15 10835 845.80 814.50 933.50 1 2019-03-16 10952 471.20 814.50 933.50 1 2019-04-09 11011 933.50 814.50 933.50 2 2017-09-18 10308 88.80 88.80 514.40 2 2018-08-08 10625 479.75 88.80 514.40 2 2018-11-28 10759 320,00 88,80 514,40 2 2019-03-04 10926 514,40 88,80 514,40 3 2017-11-27 10365 403,20 403,20 660,00 3 2018-04-15 10507 749,06 403,20 660,00 3 2018-05-13 10535 1940,85 403,20 660,00 3 2018-06-19 10573 2082,00 403,20 660,00 3 2018-09-22 10677 813,37 403,20 660,00 3 2018-09-25 10682 375,50 403,20 660,00 3 2019-01-28 10856 660,00 403,20 660,00 ...Można się zastanawiać, co było motywacją dla standardu, aby w ogóle wspierać ramę z tymi funkcjami. Jeśli się nad tym zastanowisz, użyjesz ich głównie do pobrania czegoś z pierwszego lub ostatniego wiersza w partycji. Jeśli potrzebujesz wartości z, powiedzmy, dwóch wierszy przed bieżącą, zamiast używać FIRST_VALUE z ramką, która zaczyna się od 2 PRECEDING, czy nie jest o wiele łatwiej użyć LAG z wyraźnym przesunięciem równym 2, na przykład:
SELECT custid, datazamowienia, idzamowienia, val, LAG(wal, 2) OVER( PARTITION BY custid ORDER BY datazamowienia, idzamowienia ) AS prevtwoval FROM Sales.OrderValues ORDER BY custid, datazamowienia, idzamowienia;To zapytanie generuje następujące dane wyjściowe:
identyfikator klienta data zamówienia id zamówienia val prevtwoval ------- ---------- -------- ---------- ------- ---- 1 2018-08-25 10643 814,50 NULL 1 2018-10-03 10692 878,00 NULL 1 2018-10-13 10702 330,00 814,50 1 2019-01-15 10835 845,80 878,00 1 2019-03-16 10952 471,20 330,00 1 2019-04-09 11011 933,50 845,80 2 2017-09-18 10308 88,80 NULL 2 2018-08-08 10625 479,75 NULL 2 2018-11-28 10759 320,00 88,80 2 2019-03-04 10926 514,40 479,75 3 2017-11-27 10365 403.20 NULL 3 2018-04-15 10507 749.06 NULL 3 2018-05-13 10535 1940,85 403.20 3 2018-06-19 10573 2082,00 749,06 3 2018-09-22 10677 813,37 1940,85 3 2018-09-25 10682 375,50 2082,00 3 2019 -01-28 10856 660,00 813,37 ...Najwyraźniej istnieje różnica semantyczna między powyższym użyciem funkcji LAG i FIRST_VALUE z ramką, która zaczyna się od 2 PRECEDING. W pierwszym przypadku, jeśli wiersz nie istnieje w żądanym przesunięciu, domyślnie otrzymujesz NULL. W przypadku tego ostatniego nadal otrzymujesz wartość z pierwszego wiersza, który jest obecny, tj. wartość z pierwszego wiersza w partycji. Rozważ następujące zapytanie:
SELECT custid, datazamowienia, idzamowienia, wartosc, FIRST_VALUE(wal) OVER( PARTITION BY custid ORDER BY datazamowienia, idzamowienia WIERSZE POMIĘDZY 2 POPRZEDNIM I BIEŻĄCYM WIERSZEM ) AS prevtwoval FROM Sales.OrderValues ORDER BY custid, datazamówienia, id zamówienia;; pre>To zapytanie generuje następujące dane wyjściowe:
identyfikator klienta data zamówienia id zamówienia val prevtwoval ------- ---------- -------- ---------- ------- ---- 1 2018-08-25 10643 814,50 814,50 1 2018-10-03 10692 878,00 814,50 1 2018-10-13 10702 330,00 814,50 1 2019-01-15 10835 845,80 878,00 1 2019-03-16 10952 471,20 330,00 1 2019-04-09 11011 933,50 845,80 2 2017-09-18 10308 88,80 88,80 2 2018-08-08 10625 479,75 88,80 2 2018-11-28 10759 320,00 88,80 2 2019-03-04 10926 514,40 479,75 3 2017-11-27 10365 403,20 403,20 3 2018-04-15 10507 749,06 403,20 3 2018-05-13 10535 1940,85 403,20 3 2018-06-19 10573 2082,00 749,06 3 2018-09-22 10677 813,37 1940,85 3 2018-09-25 10682 375,50 2082,00 3 2019 -01-28 10856 660,00 813,37 ...Zauważ, że tym razem w danych wyjściowych nie ma wartości NULL. Tak więc obsługa ramki z FIRST_VALUE i LAST_VALUE ma pewną wartość. Upewnij się tylko, że pamiętasz najlepszą praktykę, aby zawsze jasno określać specyfikację ramki za pomocą tych funkcji i używać opcji ROWS z minimalną ramką zawierającą wiersz, którego szukasz.
Wniosek
W tym artykule skupiono się na błędach, pułapkach i najlepszych praktykach związanych z funkcjami okien. Pamiętaj, że obie funkcje agregujące okna oraz funkcje przesunięcia okna FIRST_VALUE i LAST_VALUE obsługują ramkę i że jeśli określisz klauzulę kolejności okien, ale nie określisz jednostki ramki okna i jej skojarzonego zakresu, otrzymasz RANGE UNBOUNDED PRECEDING o domyślna. Powoduje to obniżenie wydajności, gdy zapytanie zostanie zoptymalizowane za pomocą operatorów trybu wiersza. W przypadku funkcji LAST_VALUE powoduje to pobranie wartości z bieżącego wiersza zamiast z ostatniego wiersza w partycji. Pamiętaj, aby jasno określić ramkę i generalnie preferować opcję ROWS nad RANGE. Wspaniale jest widzieć poprawę wydajności dzięki operatorowi Window Aggregate działającemu w trybie wsadowym. Kiedy ma to zastosowanie, przynajmniej pułapka wydajności jest eliminowana.