Database
 sql >> Base de données >  >> RDS >> Database

Fonctions de fenêtre imbriquées en SQL

La norme ISO/IEC 9075:2016 (SQL:2016) définit une fonctionnalité appelée fonctions de fenêtre imbriquées. Cette fonctionnalité vous permet d'imbriquer deux types de fonctions de fenêtre en tant qu'argument d'une fonction d'agrégation de fenêtre. L'idée est de vous permettre de faire référence soit à un numéro de ligne, soit à une valeur d'une expression, à des marqueurs stratégiques dans les éléments de fenêtrage. Les marqueurs vous donnent accès à la première ou à la dernière ligne de la partition, à la première ou à la dernière ligne du cadre, à la ligne extérieure actuelle et à la ligne de cadre actuelle. Cette idée est très puissante, vous permettant d'appliquer un filtrage et d'autres types de manipulations dans votre fonction de fenêtre qui sont parfois difficiles à réaliser autrement. Vous pouvez également utiliser des fonctions de fenêtre imbriquées pour émuler facilement d'autres fonctionnalités, telles que les cadres basés sur RANGE. Cette fonctionnalité n'est actuellement pas disponible dans T-SQL. J'ai posté une suggestion pour améliorer SQL Server en ajoutant la prise en charge des fonctions de fenêtre imbriquées. Assurez-vous d'ajouter votre vote si vous pensez que cette fonctionnalité pourrait vous être bénéfique.

Ce que les fonctions de fenêtre imbriquées ne concernent pas

À la date d'écriture de cet article, il n'y a pas beaucoup d'informations disponibles sur les véritables fonctions de fenêtre imbriquées standard. Ce qui rend les choses plus difficiles, c'est que je ne connais aucune plate-forme qui ait encore implémenté cette fonctionnalité. En fait, l'exécution d'une recherche sur le Web pour les fonctions de fenêtre imbriquées renvoie principalement une couverture et des discussions sur l'imbrication des fonctions d'agrégat groupées dans les fonctions d'agrégat fenêtré. Par exemple, supposons que vous souhaitiez interroger la vue Sales.OrderValues ​​dans l'exemple de base de données TSQLV5 et renvoyer pour chaque client et date de commande, le total quotidien des valeurs de commande et le total cumulé jusqu'au jour en cours. Une telle tâche implique à la fois le regroupement et le fenêtrage. Vous regroupez les lignes par numéro de client et date de commande, et appliquez une somme cumulée en plus de la somme de groupe des valeurs de commande, comme ceci :

  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;

Cette requête génère la sortie suivante, présentée ici sous forme abrégée :

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

Même si cette technique est plutôt cool, et même si les recherches sur le Web pour les fonctions de fenêtre imbriquées renvoient principalement de telles techniques, ce n'est pas ce que la norme SQL entend par fonctions de fenêtre imbriquées. Comme je n'ai trouvé aucune information sur le sujet, j'ai juste dû le comprendre à partir de la norme elle-même. Espérons que cet article augmentera la prise de conscience de la véritable fonctionnalité des fonctions de fenêtre imbriquées et incitera les gens à se tourner vers Microsoft et à demander d'ajouter une prise en charge dans SQL Server.

En quoi consistent les fonctions de fenêtre imbriquées

Les fonctions de fenêtre imbriquées incluent deux fonctions que vous pouvez imbriquer en tant qu'argument d'une fonction d'agrégation de fenêtre. Il s'agit de la fonction de numéro de ligne imbriquée et de l'expression value_of imbriquée à la fonction de ligne.

Fonction de numéro de ligne imbriquée

La fonction de numéro de ligne imbriquée vous permet de vous référer au numéro de ligne des marqueurs stratégiques dans les éléments de fenêtrage. Voici la syntaxe de la fonction :

(ROW_NUMBER()>) OVER()

Les marqueurs de ligne que vous pouvez spécifier sont :

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

Les quatre premiers marqueurs sont explicites. Comme pour les deux derniers, le marqueur CURRENT_ROW représente la ligne externe actuelle et FRAME_ROW représente la ligne de cadre interne actuelle.

Comme exemple d'utilisation de la fonction de numéro de ligne imbriquée, considérez la tâche suivante. Vous devez interroger la vue Sales.OrderValues ​​et renvoyer pour chaque commande certains de ses attributs, ainsi que la différence entre la valeur actuelle de la commande et la moyenne du client, mais en excluant la première et la dernière commande client de la moyenne.

Cette tâche est réalisable sans fonctions de fenêtre imbriquées, mais la solution implique plusieurs étapes :

  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;

Voici le résultat de cette requête, présenté ici sous forme abrégée :

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

En utilisant les fonctions de nombre de lignes imbriquées, la tâche est réalisable avec une seule requête, comme ceci :

  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;

En outre, la solution actuellement prise en charge nécessite au moins un tri dans le plan et plusieurs passages sur les données. La solution utilisant des fonctions de numéro de ligne imbriquées a tout le potentiel d'être optimisée en s'appuyant sur l'ordre de l'index et un nombre réduit de passages sur les données. Ceci, bien sûr, dépend de la mise en œuvre.

Expression imbriquée value_of à la fonction de ligne

La fonction imbriquée value_of expression at row vous permet d'interagir avec une valeur d'une expression au niveau des mêmes marqueurs de ligne stratégiques mentionnés précédemment dans un argument d'une fonction d'agrégation de fenêtre. Voici la syntaxe de cette fonction :

( VALUE OF AT [] [, ]
>) SUR()

Comme vous pouvez le voir, vous pouvez spécifier un certain delta négatif ou positif par rapport au marqueur de ligne, et éventuellement fournir une valeur par défaut au cas où une ligne n'existe pas à la position spécifiée.

Cette capacité vous donne beaucoup de puissance lorsque vous devez interagir avec différents points dans les éléments de fenêtrage. Considérez le fait que, aussi puissantes que les fonctions de fenêtre puissent être comparées à des outils alternatifs comme les sous-requêtes, ce que les fonctions de fenêtre ne prennent pas en charge est un concept de base d'une corrélation. En utilisant le marqueur CURRENT_ROW, vous avez accès à la ligne externe et émulez ainsi les corrélations. En même temps, vous bénéficiez de tous les avantages des fonctions de fenêtre par rapport aux sous-requêtes.

Par exemple, supposons que vous deviez interroger la vue Sales.OrderValues ​​et renvoyer pour chaque commande certains de ses attributs, ainsi que la différence entre la valeur actuelle de la commande et la moyenne du client, mais en excluant les commandes passées à la même date que la date de commande en cours. Cela nécessite une capacité similaire à une corrélation. Avec la fonction d'expression value_of imbriquée à la ligne, en utilisant le marqueur CURRENT_ROW, cela est réalisable facilement comme ceci :

  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;

Cette requête est censée générer la sortie suivante :

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

Si vous pensez que cette tâche est réalisable tout aussi facilement avec des sous-requêtes corrélées, dans ce cas simpliste, vous auriez raison. La même chose peut être obtenue avec la requête suivante :

  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;

Cependant, rappelez-vous qu'une sous-requête fonctionne sur une vue indépendante des données, alors qu'une fonction de fenêtre fonctionne sur l'ensemble fourni en entrée à l'étape de traitement de la requête logique qui gère la clause SELECT. Habituellement, la requête sous-jacente a une logique supplémentaire comme les jointures, les filtres, le regroupement, etc. Avec les sous-requêtes, vous devez soit préparer un CTE préliminaire, soit répéter la logique de la requête sous-jacente également dans la sous-requête. Avec les fonctions de fenêtre, il n'est pas nécessaire de répéter la logique.

Par exemple, supposons que vous n'étiez censé opérer que sur les commandes expédiées (où la date d'expédition n'est pas NULL) qui ont été traitées par l'employé 3. La solution avec la fonction de fenêtre n'a besoin d'ajouter les prédicats de filtre qu'une seule fois, comme ceci :

   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;

Cette requête est censée générer la sortie suivante :

  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 solution avec la sous-requête doit ajouter deux fois les prédicats de filtre, une fois dans la requête externe et une fois dans la sous-requête, comme ceci :

  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;

C'est soit cela, soit l'ajout d'un CTE préliminaire qui s'occupe de tout le filtrage et de toute autre logique. Quoi qu'il en soit, avec les sous-requêtes, il y a plus de couches de complexité impliquées.

L'autre avantage des fonctions de fenêtre imbriquées est que si nous avions pris en charge celles de T-SQL, il aurait été facile d'émuler le support complet manquant pour l'unité de cadre de fenêtre RANGE. L'option RANGE est censée vous permettre de définir des cadres dynamiques basés sur un décalage par rapport à la valeur de classement dans la ligne actuelle. Par exemple, supposons que vous deviez calculer pour chaque commande client à partir de la vue Sales.OrderValues ​​la valeur moyenne mobile des 14 derniers jours. Selon la norme SQL, vous pouvez y parvenir en utilisant l'option RANGE et le type INTERVAL, comme ceci :

  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;

Cette requête est censée générer la sortie suivante :

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

À la date d'écriture, cette syntaxe n'est pas prise en charge dans T-SQL. Si nous avions pris en charge les fonctions de fenêtre imbriquées dans T-SQL, vous auriez pu émuler cette requête avec le code suivant :

  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;

Qu'est-ce qu'il ne faut pas aimer ?

Votez

Les fonctions de fenêtre imbriquées standard semblent être un concept très puissant qui permet une grande flexibilité dans l'interaction avec différents points dans les éléments de fenêtrage. Je suis assez surpris de ne pas trouver de couverture du concept autre que dans la norme elle-même, et que je ne vois pas beaucoup de plates-formes le mettre en œuvre. Espérons que cet article augmentera la sensibilisation à cette fonctionnalité. Si vous pensez qu'il pourrait vous être utile de l'avoir disponible dans T-SQL, assurez-vous de voter !