sql >> Databasteknik >  >> RDS >> Database

Kapslade fönsterfunktioner i SQL

ISO/IEC 9075:2016-standarden (SQL:2016) definierar en funktion som kallas kapslade fönsterfunktioner. Den här funktionen låter dig kapsla två typer av fönsterfunktioner som ett argument för en fönsteraggregatfunktion. Tanken är att låta dig referera till antingen ett radnummer, eller till ett värde på ett uttryck, vid strategiska markörer i fönsterelement. Markörerna ger dig tillgång till den första eller sista raden i partitionen, den första eller sista raden i ramen, den aktuella yttre raden och den aktuella ramraden. Denna idé är mycket kraftfull, vilket gör att du kan tillämpa filtrering och andra typer av manipulationer inom din fönsterfunktion som ibland är svåra att åstadkomma annars. Du kan också använda kapslade fönsterfunktioner för att enkelt emulera andra funktioner, som RANGE-baserade ramar. Den här funktionen är för närvarande inte tillgänglig i T-SQL. Jag postade ett förslag för att förbättra SQL Server genom att lägga till stöd för kapslade fönsterfunktioner. Se till att lägga till din röst om du känner att den här funktionen kan vara till nytta för dig.

Vad handlar inte kapslade fönsterfunktioner om

När detta skrivs finns det inte mycket information tillgänglig där ute om de verkliga standardkapslade fönsterfunktionerna. Det som gör det svårare är att jag inte känner till någon plattform som implementerat den här funktionen ännu. Att köra en webbsökning efter kapslade fönsterfunktioner returnerar i själva verket mestadels täckning av och diskussioner om kapsling av grupperade aggregerade funktioner inom fönsterbaserade aggregerade funktioner. Anta till exempel att du vill fråga vyn Sales.OrderValues ​​i TSQLV5-exempeldatabasen och returnera för varje kund och orderdatum, den dagliga summan av ordervärdena och den löpande summan fram till den aktuella dagen. En sådan uppgift innebär både gruppering och fönster. Du grupperar raderna efter kund-ID och beställningsdatum och lägger en löpande summa ovanpå gruppsumman av beställningsvärdena, så här:

  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;

Den här frågan genererar följande utdata, som visas här i förkortad form:

  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
  ...

Även om den här tekniken är ganska cool, och även om webbsökningar efter kapslade fönsterfunktioner huvudsakligen returnerar sådana tekniker, är det inte vad SQL-standarden menar med kapslade fönsterfunktioner. Eftersom jag inte kunde hitta någon information där ute om ämnet, var jag bara tvungen att ta reda på det från själva standarden. Förhoppningsvis kommer den här artikeln att öka medvetenheten om funktionen för verkliga kapslade fönsterfunktioner och få människor att vända sig till Microsoft och be om att få lägga till stöd för det i SQL Server.

Vad handlar kapslade fönsterfunktioner om

Kapslade fönsterfunktioner inkluderar två funktioner som du kan kapsla som ett argument för en aggregerad fönsterfunktion. Dessa är den kapslade radnummerfunktionen och det kapslade värdet_av uttryck vid radfunktionen.

Inkapslad radnummerfunktion

Den kapslade radnummerfunktionen låter dig referera till radnumret med strategiska markörer i fönsterelement. Här är syntaxen för funktionen:

(ROW_NUMBER()>) ÖVER()

Radmarkörerna som du kan ange är:

  • BEGIN_PARTITION
  • END_PARTITION
  • BEGIN_FRAME
  • END_FRAME
  • CURRENT_ROW
  • FRAME_ROW

De fyra första markörerna är självförklarande. När det gäller de två sista representerar CURRENT_ROW-markören den aktuella yttre raden, och FRAME_ROW representerar den aktuella inre ramraden.

Som ett exempel för att använda den kapslade radnummerfunktionen, överväg följande uppgift. Du måste fråga i vyn Sales.OrderValues ​​och returnera några av dess attribut för varje beställning, såväl som skillnaden mellan det aktuella beställningsvärdet och kundgenomsnittet, men exklusive den första och sista kundordern från genomsnittet.

Denna uppgift kan utföras utan kapslade fönsterfunktioner, men lösningen innefattar en hel del steg:

  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;

Här är resultatet av denna fråga, som visas här i förkortad form:

  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
  ...

Genom att använda kapslade radnummerfunktioner kan uppgiften uppnås med en enda fråga, som så:

  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;

Dessutom kräver den för närvarande stödda lösningen minst en sortering i planen och flera överföringar av data. Lösningen som använder kapslade radnummerfunktioner har all potential att bli optimerad med beroende av indexordning och ett minskat antal övergångar över data. Detta är naturligtvis implementeringsberoende dock.

Inkapslat värde_av uttryck vid radfunktion

Funktionen kapslad värde_av uttryck vid rad gör att du kan interagera med ett värde för ett uttryck vid samma strategiska radmarkörer som nämnts tidigare i ett argument för en aggregerad fönsterfunktion. Här är syntaxen för denna funktion:

( VÄRDE PÅ AT [] [, ]
>) ÖVER()

Som du kan se kan du ange ett visst negativt eller positivt delta med avseende på radmarkören, och valfritt ange ett standardvärde om en rad inte finns på den angivna positionen.

Denna förmåga ger dig mycket kraft när du behöver interagera med olika punkter i fönsterelement. Tänk på det faktum att lika kraftfulla som fönsterfunktioner kan jämföras med alternativa verktyg som subqueries, vad fönsterfunktioner inte stöder är ett grundläggande koncept för en korrelation. Genom att använda CURRENT_ROW-markören får du tillgång till den yttre raden, och på så sätt emulerar korrelationer. Samtidigt får du dra nytta av alla fördelar som fönsterfunktioner har jämfört med underfrågor.

Anta som ett exempel att du behöver fråga i vyn Sales.OrderValues ​​och returnera några av dess attribut för varje beställning, såväl som skillnaden mellan det aktuella beställningsvärdet och kundgenomsnittet, men exklusive beställningar som görs samma datum som det aktuella beställningsdatumet. Detta kräver en förmåga som liknar en korrelation. Med funktionen för kapslade uttrycksvärde vid rad, med CURRENT_ROW-markören, kan detta enkelt uppnås så här:

  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;

Den här frågan är tänkt att generera följande utdata:

  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
  ...

Om du tror att den här uppgiften är lika lätt att uppnå med korrelerade underfrågor, i detta förenklade fall skulle du ha rätt. Detsamma kan uppnås med följande fråga:

  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;

Kom dock ihåg att en underfråga arbetar på en oberoende vy av data, medan en fönsterfunktion fungerar på uppsättningen som tillhandahålls som input till det logiska frågebearbetningssteget som hanterar SELECT-satsen. Vanligtvis har den underliggande frågan extra logik som kopplingar, filter, gruppering och sådant. Med delfrågor måste du antingen förbereda en preliminär CTE eller upprepa logiken i den underliggande frågan även i underfrågan. Med fönsterfunktioner behöver du inte upprepa någon av logiken.

Säg till exempel att du bara skulle arbeta på skickade beställningar (där leveransdatumet inte är NULL) som hanterades av anställd 3. Lösningen med fönsterfunktionen behöver bara lägga till filterpredikaten en gång, som så:

   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;

Den här frågan är tänkt att generera följande utdata:

  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
  ...

Lösningen med underfrågan måste lägga till filterpredikaten två gånger - en gång i den yttre frågan och en gång i underfrågan - som så:

  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;

Det är antingen detta eller att lägga till en preliminär CTE som tar hand om all filtrering och all annan logik. Hur som helst du tittar på det, med underfrågor är det fler komplexitetslager inblandade.

Den andra fördelen med kapslade fönsterfunktioner är att om vi hade stöd för de i T-SQL, skulle det ha varit lätt att emulera det saknade fullständiga stödet för RANGE-fönsterramsenheten. Alternativet RANGE är tänkt att göra det möjligt för dig att definiera dynamiska ramar som är baserade på en offset från beställningsvärdet i den aktuella raden. Anta till exempel att du behöver beräkna för varje kundorder från Sales.OrderValues ​​visar det glidande medelvärdet för de senaste 14 dagarna. Enligt SQL-standarden kan du uppnå detta med RANGE-alternativet och INTERVAL-typen, så här:

  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;

Den här frågan är tänkt att generera följande utdata:

  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
  ...

När detta skrivs stöds inte denna syntax i T-SQL. Om vi ​​hade stöd för kapslade fönsterfunktioner i T-SQL, skulle du ha kunnat emulera den här frågan med följande kod:

  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;

Vad ska man inte gilla?

Lägg din röst

De vanliga kapslade fönsterfunktionerna verkar vara ett mycket kraftfullt koncept som möjliggör mycket flexibilitet i interaktion med olika punkter i fönsterelement. Jag är ganska förvånad över att jag inte kan hitta någon täckning av konceptet annat än i själva standarden, och att jag inte ser många plattformar som implementerar det. Förhoppningsvis kommer den här artikeln att öka medvetenheten om den här funktionen. Om du känner att det kan vara användbart för dig att ha det tillgängligt i T-SQL, se till att lägga din röst!


  1. Hur man skapar en unik begränsning på kolumn för redan befintlig tabell - SQL Server / TSQL självstudie del 97

  2. Efterstående noll

  3. Fix Error Msg 4151 "Typen av det första argumentet till NULLIF kan inte vara NULL-konstanten eftersom typen av det första argumentet måste vara känd" i SQL Server

  4. Att få ID för en rad uppdaterade jag i SQL Server