Den här artikeln är den fjärde delen i en serie om T-SQL-buggar, fallgropar och bästa praxis. Tidigare täckte jag determinism, delfrågor och sammanfogningar. Fokus för denna månads artikel är buggar, fallgropar och bästa praxis relaterade till fönsterfunktioner. Tack Erland Sommarskog, Aaron Bertrand, Alejandro Mesa, Umachandar Jayachandran (UC), Fabiano Neves Amorim, Milos Radivojevic, Simon Sabin, Adam Machanic, Thomas Grohser, Chan Ming Man och Paul White för era idéer!
I mina exempel kommer jag att använda en exempeldatabas som heter TSQLV5. Du kan hitta skriptet som skapar och fyller denna databas här, och dess ER-diagram här.
Det finns två vanliga fallgropar som involverar fönsterfunktioner, som båda är resultatet av kontraintuitiva implicita standardinställningar som påtvingas av SQL-standarden. En fallgrop har att göra med beräkningar av löpande totaler där du får en fönsterram med det implicita RANGE-alternativet. En annan fallgrop är något relaterad, men har allvarligare konsekvenser, som involverar en implicit ramdefinition för funktionerna FIRST_VALUE och LAST_VALUE.
Fönsterram med implicit RANGE-alternativ
Vår första fallgrop involverar beräkningen av löpande summor med hjälp av en aggregerad fönsterfunktion, där du uttryckligen anger fönsterordningsklausulen, men du inte explicit anger fönsterramsenheten (ROWS eller RANGE) och dess relaterade fönsterramsutbredning, t.ex. ROWS OGRÄNSAD FÖREGÅENDE. Den implicita standarden är kontraintuitiv och dess konsekvenser kan vara överraskande och smärtsamma.
För att demonstrera denna fallgrop använder jag en tabell som heter Transaktioner med två miljoner bankkontotransaktioner med krediter (positiva värden) och debeteringar (negativa värden). Kör följande kod för att skapa tabellen Transaktioner och fylla i den med exempeldata:
STÄLL IN NOCOUNT PÅ; ANVÄND TSQLV5; -- http://tsql.solidq.com/SampleDatabases/TSQLV5.zip SLIPP TABELL OM FINNS dbo.Transactions; CREATE TABLE dbo.Transactions ( actid INT NOT NULL, tranid INT NOT NULL, val MONEY NOT NULL, CONSTRAINT PK_Transactions PRIMARY KEY(actid, tranid) -- skapar POC-index ); DECLARE @num_partitions AS INT =100, @rows_per_partition AS INT =20000; INSERT INTO dbo.Transaktioner MED (TABLOCK) (aktid, tranid, val) SELECT NP.n, RPP.n, (ABS(CHECKSUM(NEWID())%2)*2-1) * (1 + ABS(CHECKSUM( NEWID())%5)) FRÅN dbo.GetNums(1, @num_partitions) SOM NP CROSS JOIN DBO.GetNums(1, @rows_per_partition) SOM RPP;
Vår fallgrop har både en logisk sida med en potentiell logisk bugg såväl som en prestationssida med prestationsstraff. Prestationsstraffet är endast relevant när fönsterfunktionen är optimerad med processorer i radläge. SQL Server 2016 introducerar batch-mode Window Aggregate-operatorn, som tar bort prestationsstraffdelen av fallgropen, men före SQL Server 2019 används denna operator endast om du har ett columnstore-index närvarande på data. SQL Server 2019 introducerar batch-läge på rowstore-stöd, så att du kan få batch-mode-bearbetning även om det inte finns några columnstore-index på data. För att demonstrera prestandastraffet med radlägesbearbetning, om du kör kodexemplen i den här artikeln på SQL Server 2019 eller senare, eller på Azure SQL Database, använder du följande kod för att ställa in databaskompatibilitetsnivån till 140 så att för att inte aktivera batch-läge i radbutik än:
ALTER DATABASE TSQLV5 SET COMPATIBILITY_LEVEL =140;
Använd följande kod för att slå på tid och I/O-statistik i sessionen:
STÄLL IN STATISTIK TID, IO PÅ;
För att undvika att vänta på att två miljoner rader ska skrivas ut i SSMS, föreslår jag att du kör kodexemplen i det här avsnittet med alternativet Kasta resultat efter körning aktiverat (gå till Frågealternativ, Resultat, Rutnät och markera Kasta resultat efter körning).
Innan vi kommer till fallgropen, överväg följande fråga (kalla den Fråga 1) som beräknar bankkontosaldot efter varje transaktion genom att använda en löpande summa med en fönsteraggregatfunktion med en explicit ramspecifikation:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ROWS UNBOUNDED PRECEDING ) SOM balans FRÅN dbo.Transaktioner;
Planen för den här frågan, med bearbetning i radläge, visas i figur 1.
Figur 1:Plan för fråga 1, radlägesbearbetning
Planen hämtar data som förbeställts från tabellens klustrade index. Sedan använder den segment- och sekvensprojektoperatorerna för att beräkna radnummer för att ta reda på vilka rader som hör till den aktuella radens ram. Sedan använder den operatorerna Segment, Window Spool och Stream Aggregate för att beräkna fönsteraggregatets funktion. Window Spool-operatorn används för att spoola ramraderna som sedan måste aggregeras. Utan någon speciell optimering skulle planen ha behövt skriva per rad alla tillämpliga ramrader till spolen och sedan aggregera dem. Detta skulle ha resulterat i kvadratisk, eller N, komplexitet. Den goda nyheten är att när ramen börjar med UNBOUNDED PRECEDING, identifierar SQL Server fallet som ett snabbspår fall, där den helt enkelt tar den föregående radens löpande summa och lägger till den aktuella radens värde för att beräkna den aktuella radens löpande summa, vilket resulterar i linjär skalning. I detta snabbspårningsläge skriver planen bara två rader till spolen per inmatningsrad – en med aggregatet och en med detaljen.
Window Spool kan implementeras fysiskt på ett av två sätt. Antingen som en snabb in-memory spool som var speciellt designad för fönsterfunktioner, eller som en långsam on-disk spool, som i huvudsak är en tillfällig tabell i tempdb. Om antalet rader som måste skrivas till spoolen per underliggande rad kan överstiga 10 000, eller om SQL Server inte kan förutsäga antalet kommer den att använda den långsammare spoolen på disken. I vår frågeplan har vi exakt två rader skrivna till spoolen per underliggande rad, så SQL Server använder spoolen i minnet. Tyvärr finns det inget sätt att avgöra från planen vilken typ av spole du får. Det finns två sätt att ta reda på detta. En är att använda en utökad händelse som heter window_spool_ondisk_warning. Ett annat alternativ är att aktivera STATISTICS IO och att kontrollera antalet logiska läsningar som rapporterats för en tabell som heter Worktable. Ett större tal än noll betyder att du har spolen på disken. Noll betyder att du har spolen i minnet. Här är I/O-statistiken för vår fråga:
Tabell 'Arbetstabell' logiska läser:0. Tabell 'Transaktioner' logiska läser:6208.Som du kan se har vi använt in-memory-spolen. Det är i allmänhet fallet när du använder ROWS fönsterramsenhet med UNBOUNDED PRECEDING som första avgränsare.
Här är tidsstatistiken för vår fråga:
CPU-tid:4297 ms, förfluten tid:4441 ms.Det tog den här frågan cirka 4,5 sekunder att slutföra på min maskin med resultaten förkastade.
Nu till fångsten. Om du använder alternativet RANGE istället för ROWS, med samma avgränsare, kan det finnas en subtil skillnad i betydelse, men en stor skillnad i prestanda i radläge. Skillnaden i betydelse är bara relevant om du inte har total beställning, d.v.s. om du beställer av något som inte är unikt. Alternativet RADER OBEGRÄNSAD FÖREGÅENDE slutar med den aktuella raden, så vid oavgjort är beräkningen icke-deterministisk. Omvänt ser alternativet RANGE UNBOUNDED PRECEDING före den aktuella raden och inkluderar kopplingar om det finns. Den använder liknande logik som alternativet TOP WITH TIES. När du har total beställning, d.v.s. du beställer efter något unikt, det finns inga kopplingar att ta med, och därför blir ROWS och RANGE logiskt likvärdiga i ett sådant fall. Problemet är att när du använder RANGE, använder SQL Server alltid spoolen på disken under radlägesbearbetning eftersom den inte kan förutsäga hur många fler rader som kommer att inkluderas när du bearbetar en given rad. Detta kan medföra allvarliga prestationsstraff.
Tänk på följande fråga (kalla den fråga 2), som är samma som fråga 1, med endast alternativet RANGE istället för RADER:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid RANGE UNBOUNDED PRECEDING ) SOM balans FRÅN dbo.Transaktioner;
Planen för denna fråga visas i figur 2.
Figur 2:Plan för fråga 2, radlägesbearbetning
Fråga 2 är logiskt likvärdig med fråga 1 eftersom vi har total order; Men eftersom den använder RANGE optimeras den med spoolen på disken. Observera att i planen för fråga 2 ser Window Spool ut på samma sätt som i planen för fråga 1, och de uppskattade kostnaderna är desamma.
Här är tid- och I/O-statistik för exekvering av fråga 2:
CPU-tid:19515 ms, förfluten tid:20201 ms.Tabell 'Arbetstabell' logiska läser:12044701. Tabell 'Transaktioner' logiska läser:6208.
Lägg märke till det stora antalet logiska läsningar mot Worktable, vilket indikerar att du har spolen på disken. Körtiden är mer än fyra gånger längre än för fråga 1.
Om du tänker att om så är fallet kommer du helt enkelt att undvika att använda RANGE-alternativet, såvida du inte verkligen behöver inkludera slipsar, det är bra att tänka. Problemet är att om du använder en fönsterfunktion som stöder en ram (aggregat, FIRST_VALUE, LAST_VALUE) med en explicit fönsterordningsklausul, men inget omnämnande av fönsterramsenheten och dess tillhörande omfattning, får du RANGE UNBOUNDED PRECEDING som standard . Denna standard dikteras av SQL-standarden, och standarden valde den eftersom den i allmänhet föredrar mer deterministiska alternativ som standard. Följande fråga (kalla den Fråga 3) är ett exempel som faller i denna fälla:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ) SOM balans FRÅN dbo.Transaktioner;
Ofta skriver folk så här förutsatt att de får RADER UNBOUNDED PRECEDING som standard, utan att inse att de faktiskt får RANGE UNBOUNDED PRECEDING. Saken är att eftersom funktionen använder total ordning, får du samma resultat som med ROWS, så du kan inte se att det finns ett problem från resultatet. Men prestationssiffrorna som du kommer att få är som för fråga 2. Jag ser människor falla i den här fällan hela tiden.
Den bästa praxisen för att undvika detta problem är i de fall där du använder en fönsterfunktion med en ram, är tydlig om fönsterkarmenheten och dess omfattning och i allmänhet föredrar ROWS. Reservera användningen av RANGE endast i fall där beställningen inte är unik och du måste inkludera slipsar.
Tänk på följande fråga som illustrerar ett fall när det finns en begreppsmässig skillnad mellan ROWS och RANGE:
SELECT orderdate, orderid, val, SUM(val) OVER( ORDER BY orderdate ROWS UNBOUNDED PRECEDING ) AS sumrows, SUM(val) OVER( ORDER BY orderdate RANGE UNBOUNDED PRECEDING ) AS sumrange FROM Sales.OrderValues ORDER BY orderdate; /pre>Den här frågan genererar följande utdata:
orderdate orderid 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 ...Observera skillnaden i resultaten för de rader där samma beställningsdatum visas mer än en gång, vilket är fallet för den 8 juli 2017. Lägg märke till hur alternativet ROWS inte inkluderar kopplingar och därför är icke-deterministiskt, och hur alternativet RANGE gör inkluderar band, och är därför alltid deterministiskt.
Det är dock tveksamt om du i praktiken har fall där du beställer efter något som inte är unikt, och du verkligen behöver inkludera band för att göra beräkningen deterministisk. Vad som förmodligen är mycket vanligare i praktiken är att göra en av två saker. Den ena är att bryta banden genom att lägga till något i fönstret för att göra det unikt och på så sätt resultera i en deterministisk beräkning, som så:
SELECT orderdate, orderid, val, SUM(val) OVER(ORDER BY orderdate, orderid ROWS UNBOUNDED PRECEDING ) AS runningsum FROM Sales.OrderValues ORDER BY orderdate;Den här frågan genererar följande utdata:
orderdatum orderid val runningsum ---------- -------- ---------- ---------- 2017-07-04 10248 440.00 440.00 2017-07-05 10249 1863.40 2303.40 2017-07-08 10250 1552.60 3856.00 2017-07-08 10251 654.06 4510.06 2017-07-07-09 1025255997.9.9.96 ... <Ett annat alternativ är att tillämpa preliminär gruppering, i vårt fall, efter orderdatum, som så:
SELECT orderdate, SUM(val) AS daytotal, SUM(SUM(val)) OVER(ORDER BY orderdate ROWS UNBOUNDED PRECEDING ) AS runningsum FROM Sales.OrderValues GROUP BY orderdate ORDER BY orderdate;Den här frågan genererar följande utdata där varje beställningsdatum endast visas en gång:
orderdate daytotal runningsum ---------- ---------- ---------- 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 ...Se i alla fall till att komma ihåg den bästa praxisen här!
Den goda nyheten är att om du kör på SQL Server 2016 eller senare och har ett columnstore-index på data (även om det är ett falskt filtrerat columnstore-index), eller om du kör på SQL Server 2019 eller senare, eller i Azure SQL Database, oavsett närvaron av kolumnbutiksindex, optimeras alla tre ovannämnda frågor med batch-mode Window Aggregate-operatorn. Med den här operatören elimineras många av ineffektiviteten i radlägesbehandlingen. Den här operatören använder inte en spool alls, så det finns inga problem med in-memory kontra on-disk spool. Den använder mer sofistikerad bearbetning där den kan tillämpa flera parallella övergångar över fönstret med rader i minnet för både ROWS och RANGE.
För att demonstrera hur du använder batch-lägesoptimering, se till att din databaskompatibilitetsnivå är inställd på 150 eller högre:
ALTER DATABASE TSQLV5 SET COMPATIBILITY_LEVEL =150;Kör fråga 1 igen:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ROWS UNBOUNDED PRECEDING ) SOM balans FRÅN dbo.Transaktioner;Planen för denna fråga visas i figur 3.
Figur 3:Plan för fråga 1, batchlägesbearbetning
Här är resultatstatistiken som jag fick för den här frågan:
CPU-tid:937 ms, förfluten tid:983 ms.
Tabell 'Transaktioner' lyder logiskt:6208.Körtiden sjönk till 1 sekund!
Kör fråga 2 med det explicita RANGE-alternativet igen:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid RANGE UNBOUNDED PRECEDING ) SOM balans FRÅN dbo.Transaktioner;Planen för denna fråga visas i figur 4.
Figur 2:Plan för fråga 2, batchlägesbearbetning
Här är resultatstatistiken som jag fick för den här frågan:
CPU-tid:969 ms, förfluten tid:1048 ms.
Tabell 'Transaktioner' logiskt lyder:6208.Prestandan är densamma som för fråga 1.
Kör fråga 3 igen, med det implicita RANGE-alternativet:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ) SOM balans FRÅN dbo.Transaktioner;Planen och prestationstalen är naturligtvis desamma som för fråga 2.
När du är klar kör du följande kod för att stänga av prestandastatistik:
STÄLL IN STATISTIK TID, IO AV;Glöm inte heller att stänga av alternativet Kasta resultat efter körning i SSMS.
Implicit ram med FIRST_VALUE och LAST_VALUE
Funktionerna FIRST_VALUE och LAST_VALUE är offsetfönsterfunktioner som returnerar ett uttryck från den första respektive sista raden i fönsterramen. Det knepiga med dem är att ofta när folk använder dem för första gången inser de inte att de stöder en ram, utan tror snarare att de gäller för hela partitionen.
Överväg följande försök att returnera beställningsinformation, plus värdena för kundens första och sista beställning:
SELECT custid, orderdate, orderid, val, FIRST_VALUE(val) OVER( PARTITION BY custid ORDER BY orderdate, orderid ) AS firstval, LAST_VALUE(val) OVER( PARTITION BY custid ORDER BY orderdate, orderid ) AS lastval FROM Sales. OrderValues ORDER BY custid, orderdate, orderid;Om du felaktigt tror att dessa funktioner fungerar på hela fönsterpartitionen, vilket många som använder dessa funktioner för första gången tror, förväntar du dig naturligtvis att FIRST_VALUE returnerar ordervärdet för kundens första beställning och LAST_VALUE returnerar ordervärdet för kundens senaste beställning. I praktiken stöder dessa funktioner dock en ram. Som en påminnelse, med funktioner som stöder en ram, när du anger fönsterordningsklausulen men inte fönsterramsenheten och dess tillhörande omfattning, får du RANGE UNBOUNDED PRECEDING som standard. Med funktionen FIRST_VALUE får du det förväntade resultatet, men om din fråga optimeras med radlägesoperatorer kommer du att betala straffavgiften för att använda spoolen på disken. Med funktionen LAST_VALUE är det ännu värre. Inte bara det att du kommer att betala straffavgiften för spolen på disken, utan istället för att få värdet från den sista raden i partitionen, kommer du att få värdet från den aktuella raden!
Här är resultatet av ovanstående fråga:
custid orderdate orderid val firstval lastval ------- ---------- -------- ---------- ------ ---- ---------- 1 2018-08-25 10643 814,50 814,50 814,50 1 2018-10-03 10692 878,00 814,50 878,00 1 2018-107,00 1 2018-107-02 01 01 010-13 01 01 01 3 01 01 01 01 01 01 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 ...När folk ser sådan utdata för första gången tror de ofta att SQL Server har en bugg. Men det gör det förstås inte; det är helt enkelt SQL-standardens standard. Det finns en bugg i frågan. När du inser att det finns en ram inblandad vill du vara tydlig med ramspecifikationen och använda den minsta ram som fångar raden du är ute efter. Se också till att du använder ROWS-enheten. Så, för att få den första raden i partitionen, använd FIRST_VALUE-funktionen med ramen RADER MELLAN OBEGRÄNSAD FÖREGÅENDE OCH AKTUELL RAD. För att få den sista raden i partitionen, använd LAST_VALUE-funktionen med ramen RADER MELLAN AKTUELL RADER OCH OBEGRÄNSAD FÖLJANDE.
Här är vår reviderade fråga med buggen fixad:
SELECT custid, orderdate, orderid, val, FIRST_VALUE(val) OVER( PARTITION BY custid ORDER BY orderdate, orderid RADER MELLAN OBEGRÄNSAD FÖREGÅENDE OCH AKTUELL RADER ) AS firstval, LAST_VALUE(val) OVER( PARTITION BY BY custidOR,DER orderid RADER MELLAN AKTUELL RAD OCH OBEGRÄNSAD FÖLJANDE ) AS lastval FROM Sales.OrderValues ORDER BY custid, orderdate, orderid;Den här gången får du rätt resultat:
custid orderdate orderid val firstval lastval ------- ---------- -------- ---------- ------ ---- ---------- 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-02 5 01-3 01 01 01 5 01 01 01 01 01 01 01 01 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 ...Man undrar vad som var motivet för standarden att ens stödja en ram med dessa funktioner. Om du tänker på det kommer du mest att använda dem för att få något från den första eller sista raden i partitionen. Om du behöver värdet från t.ex. två rader före strömmen, istället för att använda FIRST_VALUE med en ram som börjar med 2 PRECEDING, är det inte mycket lättare att använda LAG med en explicit offset på 2, som så:
SELECT custid, orderdate, orderid, val, LAG(val, 2) OVER( PARTITION BY custid ORDER BY orderdate, orderid ) AS prevtwoval FROM Sales.OrderValues ORDER BY custid, orderdate, orderid;Den här frågan genererar följande utdata:
custid orderdate orderid 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.20.20.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-044 10926 514.4040 4799.7-11-11-11-17-17-17-17-17-17-17-11-17-11-17-11-17-11-11-17-11-11-11-17-11-17-11-17-11-17-117 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-09-09 1062 375555.00 375.00 323 3233 3 2019 3 2019 2019 3 2019 3 2019 2019 3 2019 2019 3 2019 3 2019 2019 3 2019 2019 2019 3 2019 3 2019 2019 3 2019 3 2019 2019 3 2019 3 2019 2019 2019 201933 3233 3233 3233 3233 32333 3 2019 3 2019 3 2019 3 2019 2019 3 2019 3 2019 3 2019 2019 3 2019 3 2019 2019 3 2019 3 2019 3 2019 201 -01-28 10856 660.00 813.37 ...Tydligen finns det en semantisk skillnad mellan ovanstående användning av LAG-funktionen och FIRST_VALUE med en ram som börjar med 2 PRECEDING. Med den förra, om en rad inte finns i önskad offset, får du en NULL som standard. Med den senare får du fortfarande värdet från den första raden som finns, det vill säga värdet från den första raden i partitionen. Tänk på följande fråga:
SELECT custid, orderdate, orderid, val, FIRST_VALUE(val) OVER( PARTITION BY custid ORDER BY orderdate, orderid RADER MELLAN 2 FÖREGÅENDE OCH NUVARANDE RADER ) SOM föregående FRÅN Sales.OrderValues ORDER BY custid, orderid;Den här frågan genererar följande utdata:
custid orderdate orderid 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 ...Observera att det denna gång inte finns några NULLs i utgången. Så det finns ett visst värde i att stödja en ram med FIRST_VALUE och LAST_VALUE. Se bara till att du kommer ihåg bästa praxis att alltid vara tydlig om ramspecifikationen med dessa funktioner och att använda alternativet ROWS med den minimala ramen som innehåller raden du är ute efter.
Slutsats
Den här artikeln fokuserade på buggar, fallgropar och bästa praxis relaterade till fönsterfunktioner. Kom ihåg att både fönsteraggregationsfunktionerna och FIRST_VALUE- och LAST_VALUE-fönsterförskjutningsfunktionerna stöder en ram, och att om du anger fönsterordningssatsen men du inte anger fönsterramsenheten och dess tillhörande omfattning, får du RANGE UNBOUNDED PRECEDING by standard. Detta medför en prestationsstraff när frågan optimeras med radlägesoperatorer. Med funktionen LAST_VALUE resulterar detta i att värdena hämtas från den aktuella raden istället för den sista raden i partitionen. Kom ihåg att vara tydlig om ramen och att generellt föredra alternativet ROWS framför RANGE. Det är fantastiskt att se prestandaförbättringarna med operatören Window Aggregate i batchläge. När det är tillämpligt elimineras åtminstone prestandafällan.