sql >> Databáze >  >> RDS >> Database

Vnořené funkce okna v SQL

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:

(ROW_NUMBER()>) NAD ()

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:

( HODNOTA AT [] [, ]
>) 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!


  1. Proč dostanu PLS-00302:komponenta musí být deklarována, když existuje?

  2. Jak funguje TRY_CONVERT() na serveru SQL Server

  3. ODP.NET Oracle.ManagedDataAccess způsobí konec relace sítě ORA-12537 souboru

  4. Jak načíst dvě odezvy Json Json Object a Array