Tento článek je čtvrtým dílem série o chybách, úskalích a osvědčených postupech T-SQL. Dříve jsem se zabýval determinismem, dílčími dotazy a spojeními. Tento měsíc se článek zaměřuje na chyby, úskalí a osvědčené postupy související s funkcemi oken. Díky Erland Sommarskog, Aaron Bertrand, Alejandro Mesa, Umachandar Jayachandran (UC), Fabiano Neves Amorim, Miloš Radivojevič, Simon Sabin, Adam Machanic, Thomas Grohser, Chan Ming Man a Paul White za nabídky vašich nápadů!
Ve svých příkladech použiji ukázkovou databázi nazvanou TSQLV5. Skript, který vytváří a naplňuje tuto databázi, najdete zde a její ER diagram zde.
Existují dvě běžná úskalí týkající se okenních funkcí, z nichž obě jsou výsledkem kontraintuitivních implicitních výchozích hodnot, které ukládá standard SQL. Jedno úskalí souvisí s výpočty průběžných součtů, kde získáte rám okna s implicitní možností ROZSAH. Další úskalí do jisté míry souvisí, ale má závažnější důsledky, zahrnující implicitní definici rámce pro funkce FIRST_VALUE a LAST_VALUE.
Okenní rám s implicitní možností RANGE
Naše první úskalí zahrnuje výpočet průběžných součtů pomocí agregační funkce okna, kde explicitně specifikujete klauzuli pořadí oken, ale explicitně neurčíte jednotku okenního rámu (ROWS nebo RANGE) a související rozsah okenního rámu, např. NEOMEZENÉ PŘEDCHOZÍ. Implicitní default je kontraintuitivní a jeho důsledky mohou být překvapivé a bolestivé.
K demonstraci tohoto úskalí použiji tabulku nazvanou Transakce obsahující dva miliony transakcí na bankovních účtech s kredity (kladné hodnoty) a debety (záporné hodnoty). Spuštěním následujícího kódu vytvořte tabulku Transactions a naplňte ji ukázkovými daty:
SET NOCOUNT ON; POUŽÍVEJTE TSQLV5; -- http://tsql.solidq.com/SampleDatabases/TSQLV5.zip DROP TABLE IF EXISTS 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) -- vytvoří POC index ); DECLARE @num_partitions AS INT =100, @rows_per_partition AS INT =20000; INSERT INTO dbo.Transakce S (TABLOCK) (aktid, 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;
Naše úskalí má jak logickou stránku s potenciální logickou chybou, tak výkonnostní stránku s penalizací za výkon. Pokuta za výkon je relevantní pouze v případě, že je funkce okna optimalizována pomocí operátorů zpracování v režimu řádků. SQL Server 2016 zavádí operátor Window Aggregate v dávkovém režimu, který odstraňuje část úskalí postihující výkon, ale před SQL Serverem 2019 se tento operátor používá pouze v případě, že máte na datech index columnstore. SQL Server 2019 zavádí dávkový režim s podporou rowstore, takže můžete získat zpracování v dávkovém režimu, i když v datech nejsou žádné indexy columnstore. Chcete-li demonstrovat snížení výkonu při zpracování v režimu řádků, pokud spouštíte ukázky kódu v tomto článku na SQL Server 2019 nebo novějším nebo na Azure SQL Database, pomocí následujícího kódu nastavte úroveň kompatibility databáze na 140, aby ještě nepovolit dávkový režim v úložišti řádků:
ALTER DATABASE TSQLV5 SET COMPATIBILITY_LEVEL =140;
K zapnutí statistiky času a I/O v relaci použijte následující kód:
SET STATISTICS TIME, IO ON;
Abyste nemuseli čekat na vytištění dvou milionů řádků v SSMS, doporučuji spouštět ukázky kódu v této části se zapnutou možností Zahodit výsledky po spuštění (přejděte na Možnosti dotazu, Výsledky, Mřížka a zaškrtněte políčko Zahodit výsledky po spuštění).
Než se dostaneme k úskalí, zvažte následující dotaz (nazývejte ho Dotaz 1), který počítá zůstatek na bankovním účtu po každé transakci použitím průběžného součtu pomocí funkce agregace oken s explicitní specifikací rámce:
VYBERTE actid, tranid, val, SUM(val) NAD( ROZDĚLENÍ PODLE actid POŘADÍ PODLE tranid ŘÁDKŮ BEZ OMEZENÍ PŘEDCHOZÍM ) JAKO zůstatek OD dbo.Transactions;
Plán pro tento dotaz využívající zpracování v režimu řádků je znázorněn na obrázku 1.
Obrázek 1:Plán pro dotaz 1, zpracování v režimu řádků
Plán stahuje předobjednaná data z seskupeného indexu tabulky. Poté pomocí operátorů Segment a Sequence Project vypočítá čísla řádků, aby zjistil, které řádky patří do rámce aktuálního řádku. Potom použije operátory Segment, Window Spool a Stream Aggregate k výpočtu funkce agregace oken. Operátor Window Spool se používá k řazení řádků rámců, které je pak třeba agregovat. Bez jakékoli speciální optimalizace by plán musel zapsat na řádek všechny příslušné řádky rámců do zařazování a poté je agregovat. To by vedlo ke kvadratické, neboli N, složitosti. Dobrou zprávou je, že když rámec začíná NEBOUZENÝM PŘEDCHOZÍM, SQL Server identifikuje případ jako zrychlený případ, ve kterém jednoduše vezme průběžný součet předchozího řádku a přidá hodnotu aktuálního řádku pro výpočet průběžného součtu aktuálního řádku, což má za následek lineární měřítko. V tomto zrychleném režimu plán zapíše do cívky pouze dva řádky na vstupní řádek – jeden s agregací a jeden s detailem.
Window Spool může být fyzicky implementován jedním ze dvou způsobů. Buď jako rychlé zařazování v paměti, které bylo speciálně navrženo pro funkce okna, nebo jako pomalé zařazování na disku, což je v podstatě dočasná tabulka v databázi tempdb. Pokud počet řádků, které je třeba zapsat do zařazování na podkladový řádek může překročit 10 000, nebo pokud SQL Server nemůže předpovědět číslo, použije pomalejší zařazování na disku. V našem plánu dotazů máme přesně dva řádky zapsané do zařazování na základní řádek, takže SQL Server používá zařazování v paměti. Bohužel neexistuje způsob, jak z plánu zjistit, jaký druh cívky získáváte. Existují dva způsoby, jak to zjistit. Jedním z nich je použití rozšířené události s názvem window_spool_ondisk_warning. Další možností je povolit STATISTICS IO a zkontrolovat počet logických čtení hlášených pro tabulku s názvem Worktable. Číslo větší než nula znamená, že máte na disku cívku. Nula znamená, že máte cívku v paměti. Zde je statistika I/O pro náš dotaz:
Logické čtení tabulky 'Worktable':0. Logické čtení tabulky 'Transakce':6208.Jak můžete vidět, použili jsme cívku v paměti. To je obecně případ, kdy používáte jednotku okenního rámu ROWS s NEBOUNDED PRECEDING jako prvním oddělovačem.
Zde jsou časové statistiky pro náš dotaz:
CPU čas:4297 ms, uplynulý čas:4441 ms.Dokončení tohoto dotazu na mém počítači trvalo asi 4,5 sekundy s výsledky zahozenými.
Nyní k úlovku. Pokud místo ŘÁDKŮ použijete možnost ROZSAH se stejnými oddělovači, může existovat nepatrný rozdíl ve významu, ale velký rozdíl ve výkonu v režimu řádků. Rozdíl ve významu je relevantní pouze v případě, že nemáte úplné uspořádání, tj. pokud objednáváte podle něčeho, co není jedinečné. Volba PŘEDCHOZÍ ŘÁDKY BEZ ODPOVĚDNOSTI končí u aktuálního řádku, takže v případě shody je výpočet nedeterministický. A naopak, možnost PŘEDCHOZÍ PŘEDCHOZÍ ROZSAH se dívá před aktuální řádek a zahrnuje remízy, pokud jsou přítomny. Používá podobnou logiku jako možnost NAHORU S TIEŽMI. Když máte totální uspořádání, tj. objednáváte podle něčeho jedinečného, neexistují žádné vazby, které by bylo třeba zahrnout, a proto se ŘÁDKY a ROZSAH v takovém případě stávají logicky ekvivalentními. Problém je v tom, že když používáte RANGE, SQL Server vždy používá zařazování na disku při zpracování v režimu řádků, protože při zpracování daného řádku nemůže předpovědět, kolik dalších řádků bude zahrnuto. To může mít přísnou výkonnost.
Zvažte následující dotaz (nazývejte ho Dotaz 2), který je stejný jako Dotaz 1, pouze s použitím možnosti ROZSAH namísto ŘÁDKŮ:
VYBERTE actid, tranid, val, SUM(val) NAD( ROZDĚLENÍ PODLE actid POŘADÍ PODLE tranid ROZSAH BEZ OMEZENÍ PŘEDCHOZÍM ) JAKO zůstatek OD dbo.Transactions;
Plán pro tento dotaz je znázorněn na obrázku 2.
Obrázek 2:Plán pro dotaz 2, zpracování v režimu řádků
Dotaz 2 je logicky ekvivalentní Dotazu 1, protože máme celkovou objednávku; protože však používá RANGE, optimalizuje se pomocí cívky na disku. Všimněte si, že v plánu pro Dotaz 2 vypadá Window Spool stejně jako v plánu pro Dotaz 1 a odhadované náklady jsou stejné.
Zde jsou statistiky času a I/O pro provedení Query 2:
Čas CPU:19515 ms, uplynulý čas:20201 ms.Logické čtení tabulky 'Worktable':12044701. Logické čtení tabulky 'Transakce':6208.
Všimněte si velkého počtu logických čtení proti Worktable, což naznačuje, že máte na disku cívku. Doba běhu je více než čtyřikrát delší než u Dotazu 1.
Pokud si myslíte, že pokud je to tak, jednoduše se vyhnete použití možnosti ROZSAH, pokud opravdu nepotřebujete zahrnout kravaty, je to dobré uvažování. Problém je v tom, že pokud použijete funkci okna, která podporuje rámec (agregáty, FIRST_VALUE, LAST_VALUE) s explicitní klauzulí o pořadí okna, ale bez zmínky o jednotce okenního rámu a jejím přidruženém rozsahu, dostanete ve výchozím nastavení RANGE UBOUNDED PRECEDING . Tato výchozí hodnota je diktována standardem SQL a standard ji zvolil, protože obecně preferuje jako výchozí hodnoty determinističtější možnosti. Následující dotaz (nazývejte jej Dotaz 3) je příkladem, který spadá do této pasti:
VYBERTE actid, tranid, val, SUM(val) NAD( ROZDĚLENÍ PODLE actid ORDER BY tranid ) JAKO zůstatek FROM dbo.Transactions;
Lidé často píší takto, za předpokladu, že ve výchozím nastavení dostávají ŘÁDKY NEBOUZENÉ PŘEDCHÁZEJÍCÍ, aniž by si uvědomovali, že ve skutečnosti dostávají PŘEDCHOZÍ BEZ ŘADY. Jde o to, že protože funkce používá celkové pořadí, dostanete stejný výsledek jako u ROWS, takže z výsledku nemůžete poznat, že je problém. Ale čísla výkonu, která získáte, jsou jako u Dotazu 2. Vidím, že lidé neustále padají do této pasti.
Nejlepší postup, jak se tomuto problému vyhnout, je v případech, kdy používáte funkci okna s rámem, vyjadřujete se explicitně k jednotce rámu okna a jeho rozsahu a obecně dáváte přednost ŘÁDKŮM. Vyhraďte si použití RANGE pouze pro případy, kdy objednávka není jedinečná a musíte zahrnout vazby.
Zvažte následující dotaz ilustrující případ, kdy existuje koncepční rozdíl mezi ŘÁDKY a ROZSAH:
VYBERTE datum objednávky, ID objednávky, hodnota, SOUČET(hodnota) NAD( ORDER BY ŘÁDKY NEOKÁZANÉ PŘEDCHÁZEJÍCÍ ) JAKO součty, SOUČET(hodnota) NAD( OBJEDNÁVKA PODLE data objednávky ROZSAH NEODPOVĚDNÝ PŘEDCHOZÍ ) JAKO součet FROM Sales.OrderValues ORDER BY orderdate;Tento dotaz generuje následující výstup:
orderdate orderid val sumrows sumrow range ---------- -------- -------- -------- --------- -2017-07-04 10248 440,00 440,00 440,00 2017-07-05 10249 1863.40 2303.40 2303,40 2017-07-08 10250 150060600606006060606060606060 6060600606060606006060606060060060610606060600600600600600600600600600600600600600600600600600600600600600600600600601060060601060. /před>Sledujte rozdíl ve výsledcích pro řádky, kde se stejné datum objednávky objevuje více než jednou, jako je tomu v případě 8. července 2017. Všimněte si, že možnost ŘÁDKY nezahrnuje remízu, a proto je nedeterministická, a jak možnost ROZSAH zahrnuje vazby, a proto je vždy deterministický.
Je však sporné, zda v praxi máte případy, kdy objednáváte podle něčeho, co není jedinečné, a opravdu potřebujete zahrnutí vazeb, aby byl výpočet deterministický. Co je v praxi pravděpodobně mnohem běžnější, je udělat jednu ze dvou věcí. Jedním z nich je přerušit vazby přidáním něčeho do uspořádání okna, aby bylo jedinečné, a výsledkem je deterministický výpočet, například takto:
SELECT orderdate, orderid, val, SUM(val) OVER( ORDER BY orderdate, orderid ŘÁDKY BEZ OMEZENÍ PŘEDCHOZÍ ) JAKO průběžný součet FROM Sales.OrderValues ORDER BY orderdate;Tento dotaz generuje následující výstup:
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 ...Další možností je použít předběžné seskupení, v našem případě podle data objednávky, např.:VYBERTE datum objednávky, SUM(hodnota) JAKO dencelkem, SOUČET(SOUČET(hodnota)) NAD( OBJEDNÁVKA PODLE data objednávky ŘÁDKY BEZ OMEZENÍ PŘEDCHOZÍ ) JAKO průběžný součet OD Sales.OrderValues GROUP BY datum objednávky ORDER BY datum objednávky;Tento dotaz generuje následující výstup, kde se každé datum objednávky objeví pouze jednou:
datum objednávky dencelkový průběžný součet ---------- --------- ----------- 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 ...V každém případě si zde nezapomeňte zapamatovat osvědčené postupy!
Dobrou zprávou je, že pokud používáte SQL Server 2016 nebo novější a máte v datech přítomen index columnstore (i když se jedná o falešný filtrovaný index columnstore), nebo pokud používáte SQL Server 2019 nebo novější, nebo v Azure SQL Database, bez ohledu na přítomnost indexů columnstore, se všechny tři výše uvedené dotazy optimalizují pomocí operátoru Window Aggregate v dávkovém režimu. S tímto operátorem je odstraněno mnoho neefektivností zpracování v režimu řádků. Tento operátor vůbec nepoužívá zařazování, takže nevzniká žádný problém se zařazováním v paměti a na disku. Využívá propracovanější zpracování, kde může aplikovat více paralelních průchodů přes okno řádků v paměti pro ROWS i RANGE.
Chcete-li předvést použití optimalizace dávkového režimu, ujistěte se, že je úroveň kompatibility vaší databáze nastavena na 150 nebo vyšší:
ALTER DATABASE TSQLV5 SET COMPATIBILITY_LEVEL =150;Spusťte dotaz 1 znovu:
VYBERTE actid, tranid, val, SUM(val) NAD( ROZDĚLENÍ PODLE actid POŘADÍ PODLE tranid ŘÁDKŮ BEZ OMEZENÍ PŘEDCHOZÍM ) JAKO zůstatek OD dbo.Transactions;Plán pro tento dotaz je znázorněn na obrázku 3.
Obrázek 3:Plán pro dotaz 1, zpracování v dávkovém režimu
Zde jsou statistiky výkonu, které jsem získal pro tento dotaz:
Čas CPU:937 ms, uplynulý čas:983 ms.
Logické čtení tabulky 'Transakce':6208.Doba běhu klesla na 1 sekundu!
Spusťte dotaz 2 znovu s explicitní možností RANGE:
VYBERTE actid, tranid, val, SUM(val) NAD( ROZDĚLENÍ PODLE actid POŘADÍ PODLE tranid ROZSAH BEZ OMEZENÍ PŘEDCHOZÍM ) JAKO zůstatek OD dbo.Transactions;Plán pro tento dotaz je znázorněn na obrázku 4.
Obrázek 2:Plán pro dotaz 2, zpracování v dávkovém režimu
Zde jsou statistiky výkonu, které jsem získal pro tento dotaz:
Čas CPU:969 ms, uplynulý čas:1048 ms.
Logické čtení tabulky 'Transakce':6208.Výkon je stejný jako u Dotazu 1.
Spusťte dotaz 3 znovu s implicitní možností RANGE:
VYBERTE actid, tranid, val, SUM(val) NAD( ROZDĚLENÍ PODLE actid ORDER BY tranid ) JAKO zůstatek FROM dbo.Transactions;Plán a čísla výkonu jsou samozřejmě stejné jako u Dotazu 2.
Až budete hotovi, spusťte následující kód pro vypnutí statistik výkonu:
SET STATISTICS TIME, IO OFF;Nezapomeňte také v SSMS vypnout možnost Zahodit výsledky po spuštění.
Implicitní rámec s FIRST_VALUE a LAST_VALUE
Funkce FIRST_VALUE a LAST_VALUE jsou funkce offsetového okna, které vrací výraz z prvního nebo posledního řádku v rámci okna. Záludné na nich je to, že když je lidé používají poprvé, často si neuvědomí, že podporují rám, spíše si myslí, že se vztahují na celý oddíl.
Zvažte následující pokus o vrácení informací o objednávce plus hodnoty první a poslední objednávky zákazníka:
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;Pokud se nesprávně domníváte, že tyto funkce fungují na celém oddílu okna, což je přesvědčení mnoha lidí, kteří tyto funkce používají poprvé, přirozeně očekáváte, že FIRST_VALUE vrátí hodnotu objednávky první objednávky zákazníka a LAST_VALUE vrátí hodnotu hodnota objednávky poslední objednávky zákazníka. V praxi však tyto funkce rámeček podporují. Připomínáme, že u funkcí, které podporují rámec, když zadáte klauzuli pořadí okna, ale ne jednotku rámu okna a její přidružený rozsah, dostanete ve výchozím nastavení RANGE UBOUNDED PRECEDING. S funkcí FIRST_VALUE získáte očekávaný výsledek, ale pokud bude váš dotaz optimalizován pomocí operátorů v režimu řádků, zaplatíte pokutu za použití on-disk spool. S funkcí LAST_VALUE je to ještě horší. Nejen, že zaplatíte pokutu za cívku na disku, ale místo abyste získali hodnotu z posledního řádku v oddílu, získáte hodnotu z aktuálního řádku!
Zde je výstup výše uvedeného dotazu:
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,08 1 320 1.00.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 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 4030206-15 4030240,850206-04-15 10506-15-15 10506 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 ...Často, když lidé vidí takový výstup poprvé, myslí si, že SQL Server má chybu. Ale samozřejmě, že ne; je to prostě výchozí standard SQL. V dotazu je chyba. Uvědomte si, že se jedná o rámec, a chcete být explicitní ohledně specifikace rámce a použít minimální rámec, který zachycuje řádek, o který usilujete. Také se ujistěte, že používáte jednotku ROWS. Chcete-li tedy získat první řádek v oddílu, použijte funkci FIRST_VALUE s rámečkem ŘÁDKY MEZI NEBOUZENÝM PŘEDCHOZÍM A AKTUÁLNÍM ŘÁDKEM. Chcete-li získat poslední řádek v oddílu, použijte funkci LAST_VALUE s rámečkem ŘÁDKY MEZI AKTUÁLNÍM ŘÁDKEM A NÁSLEDUJÍCÍM NEODPOVĚDNÝM.
Zde je náš revidovaný dotaz s opravenou chybou:
SELECT custid, orderdate, orderid, val, FIRST_VALUE(val) OVER( PARTICE BY custid ORDER BY orderdate, orderid ŘÁDKY MEZI NEOPOJENÉ PŘEDCHOZÍM A AKTUÁLNÍM ŘÁDKEM ) AS firstval, LAST_VALUE(val) OVER( PARTICE BY custidate, ORDER BY order orderid ŘÁDKY MEZI AKTUÁLNÍM ŘÁDKEM A NÁSLEDUJÍCÍM NEZÁVISLÝM ) AS lastval FROM Sales.OrderValues ORDER BY custid, orderdate, orderid;Tentokrát získáte správný výsledek:
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 1330,01 01.00 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 5140 2 2018 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,85 5053060,85 1053060,85 10530 660,8505-004-15 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 ...Člověk si klade otázku, co bylo motivací k tomu, aby standard vůbec podporoval rám s těmito funkcemi. Pokud se nad tím zamyslíte, budete je většinou používat k získání něčeho z prvních nebo posledních řádků v oddílu. Pokud potřebujete hodnotu z řekněme dvou řádků před aktuálním, není místo použití FIRST_VALUE se snímkem, který začíná 2 PŘEDCHOZÍM, mnohem jednodušší použít LAG s explicitním offsetem 2, například takto:
SELECT custid, orderdate, orderid, val, LAG(val, 2) OVER( PARTTION BY custid ORDER BY orderdate, orderid ) AS prevtwoval FROM Sales.OrderValues ORDER BY custid, orderdate, orderid;Tento dotaz generuje následující výstup:
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 1 330,00 1 330,00 1 330,00 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 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.2082-25 10682 375050 2082000 315219-25 10682 -01-28 10856 660,00 813,37 ...Zjevně existuje sémantický rozdíl mezi výše uvedeným použitím funkce LAG a FIRST_VALUE s rámcem, který začíná 2 PŘEDCHOZÍ. V prvním případě, pokud řádek v požadovaném posunu neexistuje, dostanete ve výchozím nastavení NULL. S druhým z nich stále získáte hodnotu z prvního řádku, který je přítomen, tj. hodnotu z prvního řádku v oddílu. Zvažte následující dotaz:
SELECT custid, orderdate, orderid, val, FIRST_VALUE(val) OVER( PARTICE BY custid ORDER BY orderdate, orderid ŘÁDKY MEZI 2 PŘEDCHOZÍM A AKTUÁLNÍM ŘÁDKEM ) AS prevtwoval FROM Sales.OrderValues ORDER BY custid, orderdate, order před>Tento dotaz generuje následující výstup:
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 2019 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 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 3750555520505050555020205215 37550202000. -01-28 10856 660,00 813,37 ...Všimněte si, že tentokrát ve výstupu nejsou žádné hodnoty NULL. Takže podpora rámce s FIRST_VALUE a LAST_VALUE má určitou hodnotu. Jen se ujistěte, že si pamatujete na osvědčený postup, abyste se vždy jasně vyjádřili ke specifikaci rámce s těmito funkcemi a abyste použili možnost ŘÁDKY s minimálním rámcem, který obsahuje řádek, o který usilujete.
Závěr
Tento článek se zaměřil na chyby, úskalí a osvědčené postupy související s funkcemi oken. Pamatujte, že jak funkce agregace oken, tak funkce posunu okna FIRST_VALUE a LAST_VALUE podporují rámec, a že pokud zadáte klauzuli pořadí okna, ale neurčíte jednotku rámu okna a její přidružený rozsah, získáte RANGE UBOUNDED PRECEDING o výchozí. To způsobí snížení výkonu, když se dotaz optimalizuje pomocí operátorů v režimu řádků. S funkcí LAST_VALUE to vede k získání hodnot z aktuálního řádku namísto z posledního řádku v oddílu. Nezapomeňte být o rámci explicitní a obecně preferujte možnost ŘÁDKY před RANGE. Je skvělé vidět zlepšení výkonu operátora Window Aggregate v dávkovém režimu. Když je to možné, je alespoň eliminováno úskalí výkonu.