Lo standard ISO/IEC 9075:2016 (SQL:2016) definisce una funzionalità denominata funzioni della finestra nidificata. Questa funzione consente di annidare due tipi di funzioni di finestra come argomento di una funzione di aggregazione di finestre. L'idea è di consentire di fare riferimento a un numero di riga, oa un valore di un'espressione, a indicatori strategici negli elementi di finestra. I marcatori danno accesso alla prima o all'ultima riga nella partizione, alla prima o all'ultima riga nel frame, alla riga esterna corrente e alla riga del frame corrente. Questa idea è molto potente e ti consente di applicare filtri e altri tipi di manipolazioni all'interno della funzione della tua finestra che a volte sono difficili da ottenere altrimenti. Puoi anche utilizzare le funzioni della finestra nidificata per emulare facilmente altre funzioni, come i frame basati su RANGE. Questa funzionalità non è attualmente disponibile in T-SQL. Ho pubblicato un suggerimento per migliorare SQL Server aggiungendo il supporto per le funzioni della finestra nidificata. Assicurati di aggiungere il tuo voto se ritieni che questa funzione possa esserti utile.
Di cosa non trattano le funzioni della finestra nidificata
Alla data in cui scrivo, non ci sono molte informazioni disponibili sulle vere funzioni standard della finestra nidificata. Ciò che lo rende più difficile è che non conosco ancora nessuna piattaforma che abbia implementato questa funzionalità. In effetti, l'esecuzione di una ricerca sul Web per le funzioni di finestra nidificate restituisce principalmente la copertura e le discussioni sull'annidamento di funzioni aggregate raggruppate all'interno di funzioni di aggregazione in finestra. Si supponga, ad esempio, di voler eseguire una query sulla vista Sales.OrderValues nel database di esempio TSQLV5 e restituire per ogni cliente e data dell'ordine, il totale giornaliero dei valori dell'ordine e il totale parziale fino al giorno corrente. Tale attività coinvolge sia il raggruppamento che il windowing. Raggruppi le righe in base all'ID cliente e alla data dell'ordine e applichi una somma parziale sopra la somma del gruppo dei valori dell'ordine, in questo modo:
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;
Questa query genera il seguente output, mostrato qui in forma abbreviata:
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 ...
Anche se questa tecnica è piuttosto interessante, e anche se le ricerche sul Web di funzioni di finestra nidificate restituiscono principalmente tali tecniche, non è ciò che lo standard SQL intende per funzioni di finestra nidificate. Dal momento che non riuscivo a trovare alcuna informazione sull'argomento, dovevo solo capirlo dallo standard stesso. Si spera che questo articolo aumenterà la consapevolezza della vera funzionalità delle funzioni della finestra nidificata e indurrà le persone a rivolgersi a Microsoft e chiedere di aggiungere il supporto per esso in SQL Server.
Di cosa trattano le funzioni della finestra nidificata
Le funzioni della finestra nidificata includono due funzioni che è possibile nidificare come argomento di una funzione di aggregazione della finestra. Queste sono la funzione nidificata del numero di riga e la funzione nidificata value_of nella riga.
Funzione numero riga nidificata
La funzione del numero di riga nidificata consente di fare riferimento al numero di riga degli indicatori strategici negli elementi della finestra. Ecco la sintassi della funzione:
Gli indicatori di riga che puoi specificare sono:
- BEGIN_PARTITION
- END_PARTITION
- BEGIN_FRAME
- END_FRAME
- RIGA_CORRENTE
- FRAME_ROW
I primi quattro indicatori sono autoesplicativi. Come per gli ultimi due, il marker CURRENT_ROW rappresenta la riga esterna corrente e FRAME_ROW rappresenta la riga interna del frame corrente.
Come esempio per l'utilizzo della funzione del numero di riga nidificata, considerare l'attività seguente. È necessario eseguire una query nella vista Sales.OrderValues e restituire per ogni ordine alcuni dei suoi attributi, nonché la differenza tra il valore dell'ordine corrente e la media del cliente, ma escludendo dalla media il primo e l'ultimo ordine del cliente.
Questo compito è realizzabile senza funzioni di finestra nidificate, ma la soluzione prevede alcuni passaggi:
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;
Ecco l'output di questa query, mostrato qui in forma abbreviata:
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 ...
Utilizzando le funzioni nidificate dei numeri di riga, l'attività è realizzabile con una singola query, in questo modo:
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;
Inoltre, la soluzione attualmente supportata richiede almeno un ordinamento nel piano e più passaggi sui dati. La soluzione che utilizza le funzioni dei numeri di riga nidificate ha tutte le potenzialità per essere ottimizzata facendo affidamento sull'ordine dell'indice e un numero ridotto di passaggi sui dati. Questo, ovviamente, dipende dall'implementazione.
Valore_di espressione nidificato nella funzione riga
La funzione nidificata value_of expression at row consente di interagire con un valore di un'espressione negli stessi indicatori di riga strategici menzionati in precedenza in un argomento di una funzione di aggregazione della finestra. Ecco la sintassi di questa funzione:
>) OLTRE(
Come puoi vedere, puoi specificare un certo delta negativo o positivo rispetto all'indicatore di riga e, facoltativamente, fornire un valore predefinito nel caso in cui una riga non esista nella posizione specificata.
Questa capacità ti dà molta potenza quando devi interagire con diversi punti negli elementi della finestra. Considera il fatto che, per quanto potenti funzioni di finestra possano essere paragonate a strumenti alternativi come le sottoquery, ciò che le funzioni di finestra non supportano è un concetto di base di correlazione. Usando il marcatore CURRENT_ROW si ottiene l'accesso alla riga esterna e in questo modo si emulano le correlazioni. Allo stesso tempo, puoi beneficiare di tutti i vantaggi offerti dalle funzioni della finestra rispetto alle sottoquery.
Ad esempio, supponiamo di dover interrogare la vista Sales.OrderValues e restituire per ogni ordine alcuni dei suoi attributi, nonché la differenza tra il valore dell'ordine corrente e la media del cliente, ma escludendo gli ordini effettuati nella stessa data di la data dell'ordine corrente. Ciò richiede una capacità simile a una correlazione. Con la funzione nidificata value_of nella riga, utilizzando l'indicatore CURRENT_ROW, questo è facilmente realizzabile in questo modo:
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;
Questa query dovrebbe generare il seguente output:
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 ...
Se stai pensando che questo compito sia realizzabile altrettanto facilmente con le sottoquery correlate, in questo caso semplicistico avresti ragione. Lo stesso può essere ottenuto con la seguente query:
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;
Tuttavia, ricorda che una sottoquery opera su una vista indipendente dei dati, mentre una funzione di finestra opera sull'insieme fornito come input per la fase di elaborazione della query logica che gestisce la clausola SELECT. Di solito, la query sottostante ha una logica aggiuntiva come join, filtri, raggruppamenti e così via. Con le sottoquery, è necessario preparare un CTE preliminare o ripetere la logica della query sottostante anche nella sottoquery. Con le funzioni della finestra, non è necessario ripetere la logica.
Ad esempio, supponiamo che dovresti operare solo su ordini spediti (dove la data di spedizione non è NULL) che sono stati gestiti dal dipendente 3. La soluzione con la funzione finestra deve aggiungere i predicati del filtro solo una volta, in questo modo:
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;
Questa query dovrebbe generare il seguente output:
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 ...
La soluzione con la sottoquery deve aggiungere i predicati del filtro due volte, una nella query esterna e una nella sottoquery, in questo modo:
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;
O questo, o l'aggiunta di un CTE preliminare che si occupi di tutto il filtraggio e di qualsiasi altra logica. Ad ogni modo, con le sottoquery, sono coinvolti più livelli di complessità.
L'altro vantaggio nelle funzioni della finestra nidificata è che se avessimo il supporto per quelle in T-SQL, sarebbe stato facile emulare il supporto completo mancante per l'unità frame della finestra RANGE. L'opzione RANGE dovrebbe consentire di definire frame dinamici basati su un offset dal valore di ordinamento nella riga corrente. Si supponga, ad esempio, di dover calcolare per ogni ordine cliente dalla visualizzazione Sales.OrderValues il valore della media mobile degli ultimi 14 giorni. Secondo lo standard SQL, puoi ottenere questo risultato utilizzando l'opzione RANGE e il tipo INTERVAL, in questo modo:
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;
Questa query dovrebbe generare il seguente output:
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 ...
Alla data in cui scrivo, questa sintassi non è supportata in T-SQL. Se avessimo avuto il supporto per le funzioni della finestra nidificata in T-SQL, saresti stato in grado di emulare questa query con il codice seguente:
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;
Cosa non va?
Esprimi il tuo voto
Le funzioni standard della finestra nidificata sembrano un concetto molto potente che consente molta flessibilità nell'interazione con diversi punti negli elementi della finestra. Sono piuttosto sorpreso di non riuscire a trovare alcuna copertura del concetto se non nello standard stesso e di non vedere molte piattaforme implementarlo. Speriamo che questo articolo aumenterà la consapevolezza per questa funzione. Se ritieni che possa essere utile per te averlo disponibile in T-SQL, assicurati di esprimere il tuo voto!