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 :
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 :
>) 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 !