sql >> Database teknologi >  >> RDS >> Database

Indlejrede vinduesfunktioner i SQL

ISO/IEC 9075:2016-standarden (SQL:2016) definerer en funktion kaldet indlejrede vinduesfunktioner. Denne funktion giver dig mulighed for at indlejre to slags vinduesfunktioner som et argument for en aggregeret vinduesfunktion. Ideen er at give dig mulighed for at henvise til enten et rækkenummer eller til en værdi af et udtryk ved strategiske markører i vindueselementer. Markørerne giver dig adgang til den første eller sidste række i partitionen, den første eller sidste række i rammen, den aktuelle ydre række og den aktuelle rammerække. Denne idé er meget kraftfuld og gør det muligt for dig at anvende filtrering og andre former for manipulationer i din vinduesfunktion, som nogle gange er svære at opnå ellers. Du kan også bruge indlejrede vinduesfunktioner til nemt at efterligne andre funktioner, såsom RANGE-baserede rammer. Denne funktion er i øjeblikket ikke tilgængelig i T-SQL. Jeg sendte et forslag til forbedring af SQL Server ved at tilføje understøttelse af indlejrede vinduesfunktioner. Sørg for at tilføje din stemme, hvis du føler, at denne funktion kan være til gavn for dig.

Hvad indlejrede vinduesfunktioner ikke handler om

På datoen for denne skrivning er der ikke meget information tilgængelig derude om de ægte standard indlejrede vinduesfunktioner. Hvad der gør det sværere er, at jeg endnu ikke kender nogen platform, der implementerede denne funktion. Faktisk returnerer en websøgning efter indlejrede vinduesfunktioner for det meste dækning af og diskussioner om indlejring af grupperede aggregerede funktioner i vinduesbaserede aggregerede funktioner. Antag for eksempel, at du vil forespørge på visningen Sales.OrderValues ​​i TSQLV5-eksempeldatabasen og returnere for hver kunde og ordredato, den daglige total af ordreværdierne og den løbende total indtil den aktuelle dag. En sådan opgave involverer både gruppering og vinduesdeling. Du grupperer rækkerne efter kunde-id og ordredato og anvender en løbende sum oven på gruppesummen af ​​ordreværdierne, som sådan:

  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;

Denne forespørgsel genererer følgende output, vist her i forkortet 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
  ...

Selvom denne teknik er ret cool, og selvom websøgninger efter indlejrede vinduesfunktioner hovedsageligt returnerer sådanne teknikker, er det ikke, hvad SQL-standarden mener med indlejrede vinduesfunktioner. Da jeg ikke kunne finde nogen information derude om emnet, måtte jeg bare finde ud af det ud fra selve standarden. Forhåbentlig vil denne artikel øge bevidstheden om den ægte indlejrede vinduesfunktion og få folk til at henvende sig til Microsoft og bede om at tilføje support til det i SQL Server.

Hvad handler indlejrede vinduesfunktioner om

Indlejrede vinduesfunktioner omfatter to funktioner, som du kan indlejre som et argument for en aggregeret vinduesfunktion. Det er den indlejrede rækkenummerfunktion og den indlejrede værdi_af udtryk ved rækkefunktionen.

Indlejret rækkenummerfunktion

Funktionen indlejret rækkenummer giver dig mulighed for at henvise til rækkenummeret af strategiske markører i vindueselementer. Her er syntaksen for funktionen:

(ROW_NUMBER()>) OVER()

De rækkemarkører, du kan angive, er:

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

De første fire markører er selvforklarende. Som for de sidste to repræsenterer CURRENT_ROW-markøren den aktuelle ydre række, og FRAME_ROW repræsenterer den aktuelle indre rammerække.

Som et eksempel på brug af den indlejrede rækkenummerfunktion kan du overveje følgende opgave. Du skal forespørge på visningen Sales.OrderValues ​​og returnere nogle af dens attributter for hver ordre, såvel som forskellen mellem den aktuelle ordreværdi og kundegennemsnittet, men ekskluderer den første og sidste kundeordre fra gennemsnittet.

Denne opgave kan udføres uden indlejrede vinduesfunktioner, men løsningen involverer en del trin:

  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;

Her er outputtet af denne forespørgsel, vist her i forkortet 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
  ...

Ved at bruge indlejrede rækkenummerfunktioner kan opgaven udføres med en enkelt forespørgsel, som sådan:

  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;

Desuden kræver den aktuelt understøttede løsning mindst én sortering i planen og flere overførsler af dataene. Løsningen, der anvender indlejrede rækkenummerfunktioner, har alt potentiale til at blive optimeret med afhængighed af indeksrækkefølge og et reduceret antal gennemgange af data. Dette er selvfølgelig implementeringsafhængigt.

Indlejret værdi_af udtryk ved rækkefunktion

Funktionen indlejret værdi_af udtryk ved række gør dig i stand til at interagere med en værdi af et udtryk ved de samme strategiske rækkemarkører, der er nævnt tidligere i et argument for en aggregeret vinduesfunktion. Her er syntaksen for denne funktion:

( VÆRDI AF VED [] [, ]
>) OVER()

Som du kan se, kan du angive et vist negativt eller positivt delta i forhold til rækkemarkøren og eventuelt angive en standardværdi, hvis en række ikke eksisterer på den angivne position.

Denne funktion giver dig en masse kraft, når du skal interagere med forskellige punkter i vindueselementer. Overvej det faktum, at lige så kraftfulde som vinduesfunktioner kan sammenlignes med alternative værktøjer som underforespørgsler, er det, som vinduesfunktioner ikke understøtter, et grundlæggende koncept for en korrelation. Ved at bruge CURRENT_ROW-markøren får du adgang til den ydre række, og på denne måde emulerer korrelationer. Samtidig kan du drage fordel af alle de fordele, som vinduesfunktioner har sammenlignet med underforespørgsler.

Antag for eksempel, at du skal forespørge på visningen Sales.OrderValues ​​og returnere nogle af dens attributter for hver ordre, såvel som forskellen mellem den aktuelle ordreværdi og kundegennemsnittet, men ekskluderer ordrer afgivet på samme dato som den aktuelle ordredato. Dette kræver en evne svarende til en korrelation. Med den indlejrede værdi_af udtryk ved række-funktionen, ved hjælp af CURRENT_ROW-markøren, kan dette nemt opnås som sådan:

  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;

Denne forespørgsel skal generere følgende 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
  ...

Hvis du tænker, at denne opgave lige så let kan opnås med korrelerede underforespørgsler, har du i dette forenklede tilfælde ret. Det samme kan opnås med følgende forespørgsel:

  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;

Husk dog, at en underforespørgsel opererer på en uafhængig visning af dataene, hvorimod en vinduesfunktion fungerer på det sæt, der leveres som input til det logiske forespørgselsbehandlingstrin, der håndterer SELECT-sætningen. Normalt har den underliggende forespørgsel ekstra logik som joinforbindelser, filtre, gruppering og sådan. Med underforespørgsler skal du enten forberede en foreløbig CTE eller gentage logikken i den underliggende forespørgsel også i underforespørgslen. Med vinduesfunktioner er der ingen grund til at gentage logikken.

Sig f.eks., at du kun skulle operere på afsendte ordrer (hvor afsendelsesdatoen ikke er NULL), som blev håndteret af medarbejder 3. Løsningen med vinduesfunktionen skal kun tilføje filterprædikaterne én gang, f.eks.:

   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;

Denne forespørgsel skal generere følgende 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
  ...

Løsningen med underforespørgslen skal tilføje filterprædikaterne to gange - én gang i den ydre forespørgsel og én gang i underforespørgslen - som sådan:

  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 er enten dette eller tilføjelse af en foreløbig CTE, der tager sig af al filtreringen og enhver anden logik. Uanset hvad du ser på det, med underforespørgsler er der flere kompleksitetslag involveret.

Den anden fordel ved indlejrede vinduesfunktioner er, at hvis vi havde understøttelse af dem i T-SQL, ville det have været let at efterligne den manglende fulde understøttelse af RANGE vinduesrammeenheden. RANGE-indstillingen formodes at give dig mulighed for at definere dynamiske rammer, der er baseret på en offset fra bestillingsværdien i den aktuelle række. Antag for eksempel, at du skal beregne for hver kundeordre fra Sales.OrderValues ​​viser den glidende gennemsnitsværdi for de sidste 14 dage. I henhold til SQL-standarden kan du opnå dette ved at bruge RANGE-indstillingen og INTERVAL-typen, som sådan:

  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;

Denne forespørgsel skal generere følgende 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
  ...

På datoen for denne skrivning er denne syntaks ikke understøttet i T-SQL. Hvis vi havde understøttelse af indlejrede vinduesfunktioner i T-SQL, ville du have været i stand til at emulere denne forespørgsel med følgende kode:

  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;

Hvad kan man ikke lide?

Afgiv din stemme

Standard indlejrede vinduesfunktioner virker som et meget kraftfuldt koncept, der muliggør en masse fleksibilitet i interaktion med forskellige punkter i vindueselementer. Jeg er ret overrasket over, at jeg ikke kan finde nogen dækning af konceptet andet end i selve standarden, og at jeg ikke kan se mange platforme implementere det. Forhåbentlig vil denne artikel øge bevidstheden om denne funktion. Hvis du føler, at det kunne være nyttigt for dig at have det tilgængeligt i T-SQL, så sørg for at afgive din stemme!


  1. Kan MySQL erstatte flere tegn?

  2. Sådan ændres webporten i EBS 12.2

  3. Har T-SQL en aggregeret funktion til at sammenkæde strenge?

  4. Vigtigste teknologiændringer i E-Business Suite 12.2