Denne artikel er den fjerde del i en serie om T-SQL-fejl, faldgruber og bedste praksis. Tidligere dækkede jeg determinisme, underforespørgsler og joinforbindelser. Fokus i denne måneds artikel er fejl, faldgruber og bedste praksis relateret til vinduesfunktioner. Tak Erland Sommarskog, Aaron Bertrand, Alejandro Mesa, Umachandar Jayachandran (UC), Fabiano Neves Amorim, Milos Radivojevic, Simon Sabin, Adam Machanic, Thomas Grohser, Chan Ming Man og Paul White for at tilbyde dine ideer!
I mine eksempler vil jeg bruge en prøvedatabase kaldet TSQLV5. Du kan finde scriptet, der opretter og udfylder denne database her, og dets ER-diagram her.
Der er to almindelige faldgruber, der involverer vinduesfunktioner, som begge er resultatet af kontraintuitive implicitte standarder, der pålægges af SQL-standarden. En faldgrube har at gøre med beregninger af løbende totaler, hvor du får en vinduesramme med den implicitte RANGE mulighed. En anden faldgrube er noget relateret, men har mere alvorlige konsekvenser, der involverer en implicit rammedefinition for funktionerne FIRST_VALUE og LAST_VALUE.
Vinduersramme med implicit RANGE-indstilling
Vores første faldgrube involverer beregningen af løbende totaler ved hjælp af en aggregeret vinduesfunktion, hvor du udtrykkeligt angiver vinduesrækkefølgen, men du ikke eksplicit specificerer vinduesrammeenheden (ROWS eller RANGE) og dens relaterede vinduesrammeudstrækning, f.eks. ROWS UBEGRÆNSET FOREGÅENDE. Den implicitte standard er kontraintuitiv, og dens konsekvenser kan være overraskende og smertefulde.
For at demonstrere denne faldgrube vil jeg bruge en tabel kaldet Transaktioner med to millioner bankkontotransaktioner med kreditter (positive værdier) og debet (negative værdier). Kør følgende kode for at oprette tabellen Transaktioner og udfylde den med eksempeldata:
INDSTIL ANTAL TIL; BRUG TSQLV5; -- http://tsql.solidq.com/SampleDatabases/TSQLV5.zip DROP TABEL HVIS FINDER 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) -- opretter POC-indeks ); DECLARE @num_partitions AS INT =100, @rows_per_partition AS INT =20000; INSERT INTO dbo.Transaktioner MED (TABLOCK) (actid, tranid, val) SELECT NP.n, RPP.n, (ABS(CHECKSUM(NEWID())%2)*2-1) * (1 + ABS(CHECKSUM( NEWID())%5)) FRA dbo.GetNums(1, @num_partitions) SOM NP CROSS JOIN DBO.GetNums(1, @rows_per_partition) SOM RPP;
Vores faldgrube har både en logisk side ved sig med en potentiel logisk fejl såvel som en præstationsside med en præstationsstraf. Ydelsesstraffen er kun relevant, når vinduesfunktionen er optimeret med processorer i rækketilstand. SQL Server 2016 introducerer batch-mode Window Aggregate-operatoren, som fjerner ydeevnestraffen-delen af faldgruben, men før SQL Server 2019 bruges denne operator kun, hvis du har et columnstore-indeks til stede på dataene. SQL Server 2019 introducerer batch-tilstand på rowstore-understøttelse, så du kan få batch-mode-behandling, selvom der ikke er nogen columnstore-indekser til stede på dataene. For at demonstrere ydeevnestraffen med rækketilstandsbehandlingen, hvis du kører kodeeksemplerne i denne artikel på SQL Server 2019 eller nyere eller på Azure SQL Database, skal du bruge følgende kode til at indstille databasekompatibilitetsniveauet til 140, så ikke at aktivere batch-tilstand på rækkelager endnu:
ALTER DATABASE TSQLV5 SET COMPATIBILITY_LEVEL =140;
Brug følgende kode til at slå tid og I/O-statistik til i sessionen:
INDSTIL STATISTIK TID, IO TIL;
For at undgå at vente på, at to millioner rækker udskrives i SSMS, foreslår jeg, at du kører kodeeksemplerne i dette afsnit med indstillingen Kassér resultater efter udførelse slået til (gå til Forespørgselsindstillinger, Resultater, Gitter, og marker Kassér resultater efter udførelse).
Før vi kommer til faldgruben, skal du overveje følgende forespørgsel (kald det forespørgsel 1), som beregner bankkontoens saldo efter hver transaktion ved at anvende en løbende total ved hjælp af en aggregeret vinduesfunktion med en eksplicit rammespecifikation:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ROWS UNBOUNDED PRECEDING ) SOM saldo FRA dbo.Transaktioner;
Planen for denne forespørgsel ved hjælp af rækketilstandsbehandling er vist i figur 1.
Figur 1:Plan for forespørgsel 1, rækketilstandsbehandling
Planen trækker de forudbestilte data fra tabellens klyngede indeks. Derefter bruger den segment- og sekvensprojektoperatorerne til at beregne rækkenumre for at finde ud af, hvilke rækker der hører til den aktuelle rækkes ramme. Derefter bruger den segment-, Window Spool- og Stream Aggregate-operatorerne til at beregne vinduets aggregerede funktion. Window Spool-operatoren bruges til at spoole de rammerækker, som derefter skal aggregeres. Uden nogen særlig optimering ville planen have været nødt til at skrive alle de relevante rammerækker til spolen pr. række og derefter aggregere dem. Dette ville have resulteret i kvadratisk, eller N, kompleksitet. Den gode nyhed er, at når rammen starter med UNBOUNDED PRECEDING, identificerer SQL Server sagen som en fast track tilfælde, hvor den blot tager den forrige rækkes løbende total og tilføjer den aktuelle rækkes værdi for at beregne den aktuelle rækkes løbende total, hvilket resulterer i lineær skalering. I denne fast track-tilstand skriver planen kun to rækker til spolen pr. inputrække – en med aggregatet og en med detaljerne.
Vinduesspolen kan implementeres fysisk på en af to måder. Enten som en hurtig in-memory spool, der er specielt designet til vinduesfunktioner, eller som en langsom on-disk spool, som i bund og grund er en midlertidig tabel i tempdb. Hvis antallet af rækker, der skal skrives til spoolen pr. underliggende række kan overstige 10.000, eller hvis SQL Server ikke kan forudsige antallet, vil den bruge den langsommere spool på disken. I vores forespørgselsplan har vi præcis to rækker skrevet til spoolen pr. underliggende række, så SQL Server bruger spoolen i hukommelsen. Desværre er der ingen måde at sige ud fra planen, hvilken slags spole du får. Der er to måder at finde ud af dette på. Den ene er at bruge en udvidet hændelse kaldet window_spool_ondisk_warning. En anden mulighed er at aktivere STATISTICS IO og kontrollere antallet af logiske læsninger rapporteret for en tabel kaldet Worktable. Et større tal end nul betyder, at du har spolen på disken. Nul betyder, at du har spolen i hukommelsen. Her er I/O-statistikken for vores forespørgsel:
Tabel 'Worktable' logiske læser:0. Tabel 'Transaktioner' logiske læser:6208.Som du kan se, har vi brugt in-memory spolen. Det er generelt tilfældet, når du bruger ROWS vinduesrammeenheden med UNBOUNDED PRECEDING som den første afgrænsning.
Her er tidsstatistikken for vores forespørgsel:
CPU-tid:4297 ms, forløbet tid:4441 ms.Det tog denne forespørgsel omkring 4,5 sekunder at udføre på min maskine med resultater kasseret.
Nu til fangsten. Hvis du bruger RANGE-indstillingen i stedet for ROWS, med de samme afgrænsninger, kan der være en subtil forskel i betydning, men en stor forskel i ydeevne i rækketilstand. Forskellen i betydning er kun relevant, hvis du ikke har total bestilling, dvs. hvis du bestiller efter noget, der ikke er unikt. Indstillingen RÆKKER UBEGRÆNSET FOREGÅENDE stopper med den aktuelle række, så i tilfælde af ligheder er beregningen ikke-deterministisk. Omvendt ser RANGE UNBOUNDED PRECEDING muligheden foran den aktuelle række og inkluderer bånd, hvis de er til stede. Den bruger logik svarende til indstillingen TOP WITH TIES. Når du har total bestilling, dvs. du bestiller efter noget unikt, er der ingen bindinger at inkludere, og derfor bliver ROWS og RANGE logisk ækvivalente i et sådant tilfælde. Problemet er, at når du bruger RANGE, bruger SQL Server altid spoolen på disken under rækketilstandsbehandling, da den ved behandling af en given række ikke kan forudsige, hvor mange flere rækker der vil blive inkluderet. Dette kan have en alvorlig præstationsstraf.
Overvej følgende forespørgsel (kald det forespørgsel 2), som er det samme som forespørgsel 1, idet du kun bruger RANGE-indstillingen i stedet for ROWS:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid RANGE UNBOUNDED PRECEDING ) AS saldo FRA dbo.Transaktioner;
Planen for denne forespørgsel er vist i figur 2.
Figur 2:Plan for forespørgsel 2, behandling i rækketilstand
Forespørgsel 2 svarer logisk til forespørgsel 1, fordi vi har total ordre; men da den bruger RANGE, bliver den optimeret med spoolen på disken. Bemærk, at i planen for forespørgsel 2 ser Window Spool ud som i planen for forespørgsel 1, og de anslåede omkostninger er de samme.
Her er tids- og I/O-statistikken for udførelsen af forespørgsel 2:
CPU-tid:19515 ms, forløbet tid:20201 ms.Tabel 'Worktable' logiske læser:12044701. Tabel 'Transaktioner' logiske læser:6208.
Læg mærke til det store antal logiske læsninger mod Worktable, hvilket indikerer, at du har spolen på disken. Kørselstiden er mere end fire gange længere end for forespørgsel 1.
Hvis du tænker, at hvis det er tilfældet, vil du simpelthen undgå at bruge RANGE-indstillingen, medmindre du virkelig har brug for at inkludere slips, det er god tankegang. Problemet er, at hvis du bruger en vinduesfunktion, der understøtter en ramme (aggregater, FIRST_VALUE, LAST_VALUE) med en eksplicit vinduesrækkefølgeklausul, men ingen omtale af vinduesrammeenheden og dens tilknyttede omfang, får du som standard RANGE UNBOUNDED PRECEDING . Denne standard er dikteret af SQL-standarden, og standarden valgte den, fordi den generelt foretrækker mere deterministiske muligheder som standarder. Følgende forespørgsel (kald det Query 3) er et eksempel, der falder i denne fælde:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ) AS balance FRA dbo.Transactions;
Ofte skriver folk sådan her, hvis de antager, at de som standard får RÆKKER UBEGRÆNSET FOREGÅENDE, uden at de er klar over, at de faktisk får RANGE UNBOUNDED PRECEDING. Sagen er, at da funktionen bruger total ordre, får du det samme resultat som med ROWS, så du kan ikke se, at der er et problem ud fra resultatet. Men de præstationstal, du vil få, er ligesom for forespørgsel 2. Jeg ser folk falde i denne fælde hele tiden.
Den bedste praksis for at undgå dette problem er i tilfælde, hvor du bruger en vinduesfunktion med en ramme, er eksplicit om vinduesrammeenheden og dens udstrækning og generelt foretrækker RÆKKER. Reserver kun brugen af RANGE til tilfælde, hvor bestilling ikke er unik, og du skal inkludere slips.
Overvej følgende forespørgsel, der illustrerer et tilfælde, hvor der er en begrebsmæssig forskel mellem ROWS og 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>Denne forespørgsel genererer følgende output:
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 ...Bemærk forskellen i resultaterne for rækkerne, hvor den samme ordredato vises mere end én gang, som det er tilfældet for den 8. juli 2017. Læg mærke til, hvordan ROWS-indstillingen ikke inkluderer bindinger og derfor ikke er deterministisk, og hvordan RANGE-indstillingen gør omfatter bånd, og er derfor altid deterministisk.
Det er dog tvivlsomt, om du i praksis har tilfælde, hvor du bestiller efter noget, der ikke er unikt, og du virkelig har brug for medtagelse af bånd for at gøre beregningen deterministisk. Hvad der nok er meget mere almindeligt i praksis, er at gøre en af to ting. Den ene er at bryde båndene ved at tilføje noget til vinduet for at gøre det unikt og på denne måde resultere i en deterministisk beregning, som sådan:
SELECT orderdate, orderid, val, SUM(val) OVER(ORDER BY orderdate, orderid ROWS UNBOUNDED PRECEDING ) AS runningsum FRA Sales.OrderValues BESTIL EFTER ordredato;Denne forespørgsel genererer følgende output:
orderdate 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-09 10252 3597.90 8107.96 ...En anden mulighed er at anvende foreløbig gruppering, i vores tilfælde, efter ordredato, som sådan:
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;Denne forespørgsel genererer følgende output, hvor hver ordredato kun vises én gang:
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 ...Sørg i hvert fald for at huske den bedste praksis her!
Den gode nyhed er, at hvis du kører på SQL Server 2016 eller nyere og har et columnstore-indeks til stede på dataene (selvom det er et falsk filtreret columnstore-indeks), eller hvis du kører på SQL Server 2019 eller nyere, eller i Azure SQL Database, uanset tilstedeværelsen af kolonnelagerindekser, bliver alle tre førnævnte forespørgsler optimeret med batch-mode Window Aggregate-operatoren. Med denne operatør er mange af ineffektiviteten i rækketilstandsbehandlingen elimineret. Denne operatør bruger slet ikke en spool, så der er ingen problemer med in-memory versus on-disk spool. Den bruger mere sofistikeret behandling, hvor den kan anvende flere parallelle gennemløb over vinduet med rækker i hukommelsen for både ROWS og RANGE.
For at demonstrere brugen af batch-mode-optimering skal du sørge for, at dit databasekompatibilitetsniveau er indstillet til 150 eller højere:
ALTER DATABASE TSQLV5 SET COMPATIBILITY_LEVEL =150;Kør forespørgsel 1 igen:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ROWS UNBOUNDED PRECEDING ) SOM saldo FRA dbo.Transaktioner;Planen for denne forespørgsel er vist i figur 3.
Figur 3:Plan for forespørgsel 1, batch-mode-behandling
Her er de præstationsstatistikker, jeg fik for denne forespørgsel:
CPU-tid:937 ms, forløbet tid:983 ms.
Tabel 'Transaktioner' lyder logisk:6208.Køretiden faldt til 1 sekund!
Kør forespørgsel 2 med den eksplicitte RANGE-indstilling igen:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid RANGE UNBOUNDED PRECEDING ) AS saldo FRA dbo.Transaktioner;Planen for denne forespørgsel er vist i figur 4.
Figur 2:Plan for forespørgsel 2, batch-mode-behandling
Her er de præstationsstatistikker, jeg fik for denne forespørgsel:
CPU-tid:969 ms, forløbet tid:1048 ms.
Tabel 'Transaktioner' lyder logisk:6208.Ydeevnen er den samme som for forespørgsel 1.
Kør forespørgsel 3 igen med den implicitte RANGE-indstilling:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ) AS balance FRA dbo.Transactions;Planen og præstationstallene er naturligvis de samme som for Query 2.
Når du er færdig, skal du køre følgende kode for at slå ydeevnestatistikker fra:
INDSTIL STATISTIK TID, IO FRA;Glem heller ikke at deaktivere indstillingen Kassér resultater efter udførelse i SSMS.
Implicit ramme med FIRST_VALUE og LAST_VALUE
Funktionerne FIRST_VALUE og LAST_VALUE er offset vinduesfunktioner, der returnerer et udtryk fra henholdsvis den første eller sidste række i vinduesrammen. Den vanskelige del ved dem er, at når folk ofte bruger dem for første gang, indser de ikke, at de understøtter en ramme, men tror snarere, at de gælder for hele partitionen.
Overvej følgende forsøg på at returnere ordreoplysninger plus værdierne af kundens første og sidste ordre:
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 FRA Salg. OrderValues BESTIL EFTER custid, orderdate, orderid;Hvis du fejlagtigt tror, at disse funktioner fungerer på hele vinduespartitionen, hvilket mange mennesker tror, der bruger disse funktioner for første gang, forventer du naturligvis, at FIRST_VALUE returnerer ordreværdien af kundens første ordre, og LAST_VALUE returnerer ordreværdi af kundens sidste ordre. I praksis understøtter disse funktioner dog en ramme. Som en påmindelse, med funktioner, der understøtter en ramme, når du angiver vinduesrækkefølgen, men ikke vinduesrammeenheden og dens tilknyttede omfang, får du som standard RANGE UNBOUNDED PRECEDING. Med FIRST_VALUE-funktionen får du det forventede resultat, men hvis din forespørgsel bliver optimeret med rækketilstandsoperatorer, betaler du straffen for at bruge spolen på disken. Med funktionen LAST_VALUE er det endnu værre. Ikke kun at du betaler bøden for spolen på disken, men i stedet for at få værdien fra den sidste række i partitionen, får du værdien fra den aktuelle række!
Her er outputtet af ovenstående forespørgsel:
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 01 07 02 01 01 3 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-080808250 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-051313555770.20.850.20.20.20.20.20.250.20.20.20.20.250.20.20.20.250.20.20.20.250.20.20.2505050505010 3 2018 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ådanne output for første gang, tror de ofte, at SQL Server har en fejl. Men det gør det selvfølgelig ikke; det er simpelthen SQL-standardens standard. Der er en fejl i forespørgslen. Når du er klar over, at der er en ramme involveret, vil du gerne være eksplicit omkring rammespecifikationen og bruge den minimumsramme, der fanger den række, du leder efter. Sørg også for, at du bruger ROWS-enheden. Så for at få den første række i partitionen, brug FIRST_VALUE-funktionen med rammen RÆKKER MELLEM UBEGRÆNSET FOREGÅENDE OG AKTUELLE RÆKKE. For at få den sidste række i partitionen skal du bruge LAST_VALUE-funktionen med rammen RÆKKER MELLEM AKTUELLE RÆKKE OG UBEGRÆNSET FØLGENDE.
Her er vores reviderede forespørgsel med fejlen rettet:
SELECT custid, orderdate, orderid, val, FIRST_VALUE(val) OVER( PARTITION BY custid ORDER BY orderdate, orderid RÆKKER MELLEM UBEGRÆNSET FOREGÅENDE OG NUVÆRENDE RÆKKE ) AS firstval, LAST_VALUE(val) OVER( PARTITION BY custidOR,DER orderid RÆKKER MELLEM NUVÆRENDE RÆKKE OG UBEGRÆNSET FØLGENDE ) SOM lastval FRA Sales.OrderValues BESTIL AF custid, orderdate, orderid;Denne gang får du det rigtige 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-107-02 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 kan spørge sig selv, hvad der var motivationen for, at standarden overhovedet understøttede en ramme med disse funktioner. Hvis du tænker over det, vil du mest bruge dem til at få noget fra den første eller sidste række i partitionen. Hvis du har brug for værdien fra f.eks. to rækker før den aktuelle, i stedet for at bruge FIRST_VALUE med en ramme, der starter med 2 PRECEDING, er det ikke meget nemmere at bruge LAG med en eksplicit offset på 2, som sådan:
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;Denne forespørgsel genererer følgende output:
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 330.00 130 2019-04-09 11011 933,50 845,80 2 2017-09-18 10308 88,80 NULL 2 2018-08-08 10625 479,75 NUL 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 ...Tilsyneladende er der en semantisk forskel mellem ovenstående brug af LAG-funktionen og FIRST_VALUE med en ramme, der starter med 2 PRECEDING. Med førstnævnte, hvis en række ikke eksisterer i den ønskede offset, får du som standard en NULL. Med sidstnævnte får du stadig værdien fra den første række, der er til stede, dvs. værdien fra den første række i partitionen. Overvej følgende forespørgsel:
SELECT custid, orderdate, orderid, val, FIRST_VALUE(val) OVER( PARTITION BY custid ORDER BY orderdate, orderid RÆKKER MELLEM 2 FOREGÅENDE OG NUVÆRENDE RÆKKE ) SOM tidligere FRA Salg.OrderValues ORDER BY custid, orderid;Denne forespørgsel genererer følgende output:
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 1335 845.80 878.00 1 2019-03-16 10952 471.20 330.00 1330 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 2010 2 2019-03-0404 10926 514,40 479,75 3277-27-22-22-20 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.092525252525252525322 32025301022 32025301 -01-28 10856 660,00 813,37 ...Bemærk, at der denne gang ikke er NULL i outputtet. Så der er en vis værdi i at understøtte en ramme med FIRST_VALUE og LAST_VALUE. Bare sørg for, at du husker den bedste praksis for altid at være eksplicit om rammespecifikationen med disse funktioner, og at bruge indstillingen ROWS med den minimale ramme, der indeholder den række, du leder efter.
Konklusion
Denne artikel fokuserede på fejl, faldgruber og bedste praksis relateret til vinduesfunktioner. Husk, at både vinduesaggregerede funktioner og FIRST_VALUE og LAST_VALUE vinduesforskydningsfunktionerne understøtter en ramme, og at hvis du angiver vinduesrækkefølgen, men du ikke specificerer vinduesrammeenheden og dens tilknyttede omfang, får du RANGE UNBOUNDED PRECEDING af Standard. Dette medfører en ydeevnestraf, når forespørgslen bliver optimeret med rækketilstandsoperatører. Med funktionen LAST_VALUE resulterer dette i at få værdierne fra den aktuelle række i stedet for den sidste række i partitionen. Husk at være eksplicit om rammen og generelt at foretrække RÆKKER frem for RANGE. Det er fantastisk at se ydeevneforbedringerne med batch-mode Window Aggregate-operatøren. Når det er relevant, er i det mindste præstationsfælden elimineret.