Norma ISO/IEC 9075:2016 (SQL:2016) definuje funkci nazývanou vnořené funkce okna. Tato funkce umožňuje vnořit dva druhy funkcí okna jako argument agregační funkce okna. Cílem je umožnit vám odkazovat buď na číslo řádku, nebo na hodnotu výrazu u strategických značek v prvcích oken. Značky vám umožňují přístup k prvnímu nebo poslednímu řádku v oddílu, prvnímu nebo poslednímu řádku v rámci, aktuálnímu vnějšímu řádku a aktuálnímu řádku rámce. Tato myšlenka je velmi účinná a umožňuje vám v rámci funkce okna použít filtrování a další druhy manipulací, kterých je někdy obtížné dosáhnout jinak. Můžete také použít vnořené funkce oken ke snadné emulaci dalších funkcí, jako jsou snímky založené na RANGE. Tato funkce není aktuálně dostupná v T-SQL. Zveřejnil jsem návrh na vylepšení serveru SQL přidáním podpory pro funkce vnořených oken. Pokud se domníváte, že by tato funkce mohla být pro vás přínosná, nezapomeňte přidat svůj hlas.
O čem vnořené funkce okna nejsou
V době psaní tohoto článku není k dispozici mnoho informací o skutečných standardních funkcích vnořených oken. O to těžší je, že zatím nevím o žádné platformě, která by tuto funkci implementovala. Spuštění webového vyhledávání pro vnořené funkce okna ve skutečnosti vrátí většinou pokrytí a diskuse o vnoření seskupených agregačních funkcí do agregovaných funkcí v okně. Předpokládejme například, že se chcete dotazovat na zobrazení Sales.OrderValues ve vzorové databázi TSQLV5 a vrátit pro každého zákazníka a datum objednávky denní součet hodnot objednávky a průběžný součet až do aktuálního dne. Takový úkol zahrnuje jak seskupování, tak vytváření oken. Řádky seskupíte podle ID zákazníka a data objednávky a použijete průběžný součet na vrchol skupinového součtu hodnot objednávky, například takto:
USE TSQLV5; -- http://tsql.solidq.com/SampleDatabases/TSQLV5.zip SELECT custid, orderdate, SUM(val) AS daytotal, SUM(SUM(val)) OVER(PARTITION BY custid ORDER BY orderdate ROWS UNBOUNDED PRECEDING) AS runningsum FROM Sales.OrderValues GROUP BY custid, orderdate;
Tento dotaz generuje následující výstup, který je zde zobrazen ve zkrácené podobě:
custid orderdate daytotal runningsum ------- ---------- -------- ---------- 1 2018-08-25 814.50 814.50 1 2018-10-03 878.00 1692.50 1 2018-10-13 330.00 2022.50 1 2019-01-15 845.80 2868.30 1 2019-03-16 471.20 3339.50 1 2019-04-09 933.50 4273.00 2 2017-09-18 88.80 88.80 2 2018-08-08 479.75 568.55 2 2018-11-28 320.00 888.55 2 2019-03-04 514.40 1402.95 ...
I když je tato technika docela skvělá a i když vyhledávání funkcí vnořených oken na webu vrací hlavně takové techniky, není to to, co standard SQL myslí vnořenými funkcemi oken. Protože jsem k tomuto tématu nenašel žádné informace, musel jsem to zjistit ze samotného standardu. Doufejme, že tento článek zvýší povědomí o skutečné funkci vnořených okenních funkcí a přiměje lidi, aby se obrátili na Microsoft a požádali o přidání podpory pro SQL Server.
O čem jsou funkce vnořených oken
Vnořené funkce okna zahrnují dvě funkce, které můžete vnořit jako argument agregační funkce okna. Jedná se o vnořenou funkci čísla řádku a vnořenou hodnotu výrazu ve funkci řádku.
Funkce čísla vnořených řádků
Funkce čísla vnořených řádků vám umožňuje odkazovat na počet řádků strategických značek v prvcích okna. Zde je syntaxe funkce:
Značky řádků, které můžete zadat, jsou:
- BEGIN_PARTITION
- END_PARTITION
- BEGIN_FRAME
- END_FRAME
- CURRENT_ROW
- FRAME_ROW
První čtyři značky jsou samozřejmé. Stejně jako u posledních dvou představuje značka CURRENT_ROW aktuální vnější řádek a FRAME_ROW představuje aktuální vnitřní řádek snímku.
Jako příklad použití funkce čísla vnořeného řádku zvažte následující úlohu. Musíte se dotázat zobrazení Sales.OrderValues a vrátit pro každou objednávku některé z jejích atributů a také rozdíl mezi aktuální hodnotou objednávky a průměrem zákazníka, ale s výjimkou první a poslední objednávky zákazníka z průměru.
Tento úkol je dosažitelný bez vnořených okenních funkcí, ale řešení zahrnuje několik kroků:
WITH C1 AS ( SELECT custid, val, ROW_NUMBER() OVER( PARTITION BY custid ORDER BY orderdate, orderid ) AS rownumasc, ROW_NUMBER() OVER( PARTITION BY custid ORDER BY orderdate DESC, orderid DESC ) AS rownumdesc FROM Sales.OrderValues ), C2 AS ( SELECT custid, AVG(val) AS avgval FROM C1 WHERE 1 NOT IN (rownumasc, rownumdesc) GROUP BY custid ) SELECT O.orderid, O.custid, O.orderdate, O.val, O.val - C2.avgval AS diff FROM Sales.OrderValues AS O LEFT OUTER JOIN C2 ON O.custid = C2.custid;
Zde je výstup tohoto dotazu, zobrazený zde ve zkrácené podobě:
orderid custid orderdate val diff -------- ------- ---------- -------- ------------ 10411 10 2018-01-10 966.80 -570.184166 10743 4 2018-11-17 319.20 -809.813636 11075 68 2019-05-06 498.10 -1546.297500 10388 72 2017-12-19 1228.80 -358.864285 10720 61 2018-10-28 550.00 -144.744285 11052 34 2019-04-27 1332.00 -1164.397500 10457 39 2018-02-25 1584.00 -797.999166 10789 23 2018-12-22 3687.00 1567.833334 10434 24 2018-02-03 321.12 -1329.582352 10766 56 2018-12-05 2310.00 1015.105000 ...
Pomocí funkcí vnořených čísel řádků je úkol dosažitelný jediným dotazem, například takto:
SELECT orderid, custid, orderdate, val, val - AVG( CASE WHEN ROW_NUMBER(FRAME_ROW) NOT IN ( ROW_NUMBER(BEGIN_PARTITION), ROW_NUMBER(END_PARTITION) ) THEN val END ) OVER( PARTITION BY custid ORDER BY orderdate, orderid ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ) AS diff FROM Sales.OrderValues;
Aktuálně podporované řešení také vyžaduje alespoň jedno řazení v plánu a vícenásobné předání dat. Řešení využívající funkce vnořených čísel řádků má veškerý potenciál k optimalizaci se spoléháním na pořadí indexů a sníženým počtem průchodů daty. To však samozřejmě závisí na implementaci.
Vnořená hodnota_výrazu ve funkci řádku
Funkce vnořená hodnota_výrazu na řádku umožňuje interakci s hodnotou výrazu na stejných strategických značkách řádků zmíněných dříve v argumentu agregační funkce okna. Zde je syntaxe této funkce:
>) PŘES (
Jak vidíte, můžete určit určitý záporný nebo kladný rozdíl vzhledem k značce řádku a volitelně poskytnout výchozí hodnotu pro případ, že řádek na zadané pozici neexistuje.
Tato schopnost vám dává spoustu výkonu, když potřebujete interagovat s různými body v prvcích oken. Zvažte skutečnost, že jakkoli výkonné funkce okna lze přirovnat k alternativním nástrojům, jako jsou poddotazy, to, co funkce okna nepodporují, je základním konceptem korelace. Pomocí značky CURRENT_ROW získáte přístup k vnější řadě a tímto způsobem emulujete korelace. Zároveň můžete těžit ze všech výhod, které funkce okna mají ve srovnání s dílčími dotazy.
Předpokládejme například, že se potřebujete dotazovat na zobrazení Sales.OrderValues a vrátit pro každou objednávku některé z jejích atributů a také rozdíl mezi aktuální hodnotou objednávky a průměrem zákazníka, ale s výjimkou objednávek zadaných ke stejnému datu jako aktuální datum objednávky. To vyžaduje schopnost podobnou korelaci. S vnořeným výrazem value_of ve funkci řádku, pomocí značky CURRENT_ROW, toho lze snadno dosáhnout takto:
SELECT orderid, custid, orderdate, val, val - AVG( CASE WHEN orderdate <> VALUE OF orderdate AT CURRENT_ROW THEN val END ) OVER( PARTITION BY custid ) AS diff FROM Sales.OrderValues;
Tento dotaz má vygenerovat následující výstup:
orderid custid orderdate val diff -------- ------- ---------- -------- ------------ 10248 85 2017-07-04 440.00 180.000000 10249 79 2017-07-05 1863.40 1280.452000 10250 34 2017-07-08 1552.60 -854.228461 10251 84 2017-07-08 654.06 -293.536666 10252 76 2017-07-09 3597.90 1735.092728 10253 34 2017-07-10 1444.80 -970.320769 10254 14 2017-07-11 556.62 -1127.988571 10255 68 2017-07-12 2490.50 617.913334 10256 88 2017-07-15 517.80 -176.000000 10257 35 2017-07-16 1119.90 -153.562352 ...
Pokud si myslíte, že tento úkol je dosažitelný stejně snadno pomocí korelovaných poddotazů, v tomto zjednodušeném případě byste měli pravdu. Totéž lze dosáhnout pomocí následujícího dotazu:
SELECT O1.orderid, O1.custid, O1.orderdate, O1.val, O1.val - ( SELECT AVG(O2.val) FROM Sales.OrderValues AS O2 WHERE O2.custid = O1.custid AND O2.orderdate <> O1.orderdate ) AS diff FROM Sales.OrderValues AS O1;
Pamatujte však, že poddotaz pracuje na nezávislém pohledu na data, zatímco funkce okna pracuje na sadě, která je poskytnuta jako vstup pro krok zpracování logického dotazu, který zpracovává klauzuli SELECT. Obvykle má základní dotaz další logiku, jako jsou spojení, filtry, seskupování a podobně. U poddotazů musíte buď připravit předběžný CTE, nebo opakovat logiku základního dotazu také v poddotazu. S funkcemi okna není třeba opakovat žádnou logiku.
Řekněme například, že jste měli pracovat pouze s odeslanými objednávkami (kde datum odeslání není NULL), které zpracoval zaměstnanec 3. Řešení s funkcí okna potřebuje přidat predikáty filtru pouze jednou, například takto:
SELECT orderid, custid, orderdate, val, val - AVG( CASE WHEN orderdate <> VALUE OF orderdate AT CURRENT_ROW THEN val END ) OVER( PARTITION BY custid ) AS diff FROM Sales.OrderValues WHERE empid = 3 AND shippeddate IS NOT NULL;
Tento dotaz má vygenerovat následující výstup:
orderid custid orderdate val diff -------- ------- ---------- -------- ------------- 10251 84 2017-07-08 654.06 -459.965000 10253 34 2017-07-10 1444.80 531.733334 10256 88 2017-07-15 517.80 -1022.020000 10266 87 2017-07-26 346.56 NULL 10273 63 2017-08-05 2037.28 -3149.075000 10283 46 2017-08-16 1414.80 534.300000 10309 37 2017-09-19 1762.00 -1951.262500 10321 38 2017-10-03 144.00 NULL 10330 46 2017-10-16 1649.00 885.600000 10332 51 2017-10-17 1786.88 495.830000 ...
Řešení s poddotazem potřebuje přidat predikáty filtru dvakrát – jednou ve vnějším dotazu a jednou v poddotazu – takto:
SELECT O1.orderid, O1.custid, O1.orderdate, O1.val, O1.val - ( SELECT AVG(O2.val) FROM Sales.OrderValues AS O2 WHERE O2.custid = O1.custid AND O2.orderdate <> O1.orderdate AND empid = 3 AND shippeddate IS NOT NULL) AS diff FROM Sales.OrderValues AS O1 WHERE empid = 3 AND shippeddate IS NOT NULL;
Je to buď toto, nebo přidání předběžného CTE, které se stará o veškeré filtrování a jakoukoli další logiku. Každopádně, když se na to podíváte, s poddotazy je zahrnuto více vrstev složitosti.
Další výhodou vnořených okenních funkcí je, že pokud bychom měli podporu pro ty v T-SQL, bylo by snadné emulovat chybějící plnou podporu pro jednotku okenního rámu RANGE. Možnost RANGE vám má umožnit definovat dynamické snímky, které jsou založeny na posunu od hodnoty řazení v aktuálním řádku. Předpokládejme například, že potřebujete pro každou objednávku zákazníka ze zobrazení Sales.OrderValues vypočítat klouzavou průměrnou hodnotu za posledních 14 dní. Podle standardu SQL toho můžete dosáhnout pomocí možnosti RANGE a typu INTERVAL, například takto:
SELECT orderid, custid, orderdate, val, AVG(val) OVER( PARTITION BY custid ORDER BY orderdate RANGE BETWEEN INTERVAL '13' DAY PRECEDING AND CURRENT ROW ) AS movingavg14days FROM Sales.OrderValues;
Tento dotaz má vygenerovat následující výstup:
orderid custid orderdate val movingavg14days -------- ------- ---------- ------- --------------- 10643 1 2018-08-25 814.50 814.500000 10692 1 2018-10-03 878.00 878.000000 10702 1 2018-10-13 330.00 604.000000 10835 1 2019-01-15 845.80 845.800000 10952 1 2019-03-16 471.20 471.200000 11011 1 2019-04-09 933.50 933.500000 10308 2 2017-09-18 88.80 88.800000 10625 2 2018-08-08 479.75 479.750000 10759 2 2018-11-28 320.00 320.000000 10926 2 2019-03-04 514.40 514.400000 10365 3 2017-11-27 403.20 403.200000 10507 3 2018-04-15 749.06 749.060000 10535 3 2018-05-13 1940.85 1940.850000 10573 3 2018-06-19 2082.00 2082.000000 10677 3 2018-09-22 813.37 813.370000 10682 3 2018-09-25 375.50 594.435000 10856 3 2019-01-28 660.00 660.000000 ...
V době psaní tohoto článku není tato syntaxe v T-SQL podporována. Pokud bychom měli podporu pro funkce vnořených oken v T-SQL, mohli byste tento dotaz emulovat pomocí následujícího kódu:
SELECT orderid, custid, orderdate, val, AVG( CASE WHEN DATEDIFF(day, orderdate, VALUE OF orderdate AT CURRENT_ROW) BETWEEN 0 AND 13 THEN val END ) OVER( PARTITION BY custid ORDER BY orderdate RANGE UNBOUNDED PRECEDING ) AS movingavg14days FROM Sales.OrderValues;
Co se vám nelíbí?
Hlasujte
Standardní funkce vnořených oken se zdají jako velmi výkonný koncept, který umožňuje velkou flexibilitu při interakci s různými body v prvcích oken. Docela mě překvapuje, že nemohu najít žádné pokrytí tohoto konceptu jinak než v samotném standardu a že nevidím mnoho platforem, které jej implementují. Doufejme, že tento článek zvýší povědomí o této funkci. Pokud si myslíte, že by pro vás mohlo být užitečné mít ji k dispozici v T-SQL, nezapomeňte hlasovat!