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

Numéros de ligne avec ordre non déterministe

La fonction de fenêtre ROW_NUMBER a de nombreuses applications pratiques, bien au-delà des besoins de classement évidents. La plupart du temps, lorsque vous calculez des numéros de ligne, vous devez les calculer en fonction d'un certain ordre et vous fournissez la spécification d'ordre souhaitée dans la clause d'ordre de fenêtre de la fonction. Cependant, il existe des cas où vous devez calculer les numéros de ligne sans ordre particulier ; en d'autres termes, basé sur un ordre non déterministe. Cela peut être sur l'ensemble du résultat de la requête ou au sein des partitions. Les exemples incluent l'attribution de valeurs uniques aux lignes de résultats, la déduplication des données et le renvoi de n'importe quelle ligne par groupe.

Notez que la nécessité d'attribuer des numéros de ligne en fonction d'un ordre non déterministe est différente de la nécessité de les attribuer en fonction d'un ordre aléatoire. Avec le premier, vous ne vous souciez pas de l'ordre dans lequel ils sont attribués, et si des exécutions répétées de la requête continuent d'attribuer les mêmes numéros de ligne aux mêmes lignes ou non. Avec ce dernier, vous vous attendez à ce que les exécutions répétées changent constamment les lignes attribuées avec quels numéros de ligne. Cet article explore différentes techniques pour calculer les numéros de ligne avec un ordre non déterministe. L'espoir est de trouver une technique à la fois fiable et optimale.

Un merci spécial à Paul White pour le conseil concernant le pliage constant, pour la technique de la constante d'exécution et pour avoir toujours été une excellente source d'informations !

Lorsque l'ordre compte

Je vais commencer par les cas où l'ordre des numéros de ligne est important.

Je vais utiliser une table appelée T1 dans mes exemples. Utilisez le code suivant pour créer ce tableau et le remplir avec des exemples de données :

SET NOCOUNT ON;
 
USE tempdb;
 
DROP TABLE IF EXISTS dbo.T1;
GO
 
CREATE TABLE dbo.T1
(
  id INT NOT NULL CONSTRAINT PK_T1 PRIMARY KEY,
  grp VARCHAR(10) NOT NULL,
  datacol INT NOT NULL
);
 
INSERT INTO dbo.T1(id, grp, datacol) VALUES
  (11, 'A', 50),
  ( 3, 'B', 20),
  ( 5, 'A', 40),
  ( 7, 'B', 10),
  ( 2, 'A', 50);

Considérez la requête suivante (nous l'appellerons la requête 1) :

SELECT id, grp, datacol,
  ROW_NUMBER() OVER(PARTITION BY grp ORDER BY datacol) AS n 
FROM dbo.T1;

Ici, vous souhaitez que les numéros de ligne soient attribués dans chaque groupe identifié par la colonne grp, triés par la colonne datacol. Lorsque j'ai exécuté cette requête sur mon système, j'ai obtenu le résultat suivant :

id  grp  datacol  n
--- ---- -------- ---
5   A    40       1
2   A    50       2
11  A    50       3
7   B    10       1
3   B    20       2

Les numéros de ligne sont attribués ici dans un ordre partiellement déterministe et partiellement non déterministe. Ce que je veux dire par là, c'est que vous avez l'assurance qu'au sein de la même partition, une ligne avec une valeur de datacol supérieure obtiendra une valeur de numéro de ligne supérieure. Cependant, étant donné que datacol n'est pas unique dans la partition grp, l'ordre d'attribution des numéros de ligne parmi les lignes ayant les mêmes valeurs grp et datacol n'est pas déterministe. C'est le cas des lignes avec les valeurs d'id 2 et 11. Les deux ont la valeur grp A et la valeur datacol 50. Lorsque j'ai exécuté cette requête sur mon système pour la première fois, la ligne avec l'id 2 a obtenu le numéro de ligne 2 et le la ligne avec l'identifiant 11 a obtenu le numéro de ligne 3. Peu importe la probabilité que cela se produise dans la pratique dans SQL Server ; si je lance à nouveau la requête, théoriquement, la ligne avec l'identifiant 2 pourrait être affectée avec le numéro de ligne 3 et la ligne avec l'identifiant 11 pourrait être affectée avec le numéro de ligne 2.

Si vous devez attribuer des numéros de ligne en fonction d'un ordre complètement déterministe, garantissant des résultats reproductibles à travers les exécutions de la requête tant que les données sous-jacentes ne changent pas, vous avez besoin que la combinaison d'éléments dans les clauses de partitionnement et d'ordonnancement de la fenêtre soit unique. Cela pourrait être réalisé dans notre cas en ajoutant l'identifiant de colonne à la clause d'ordre de fenêtre comme condition de départage. La clause OVER serait alors :

OVER (PARTITION BY grp ORDER BY datacol, id)

Quoi qu'il en soit, lors du calcul des numéros de lignes en fonction d'une spécification de classement significative, comme dans la requête 1, SQL Server doit traiter les lignes classées par la combinaison des éléments de partitionnement et de classement des fenêtres. Cela peut être réalisé soit en extrayant les données pré-ordonnées d'un index, soit en triant les données. Pour le moment, il n'y a pas d'index sur T1 pour prendre en charge le calcul ROW_NUMBER dans la requête 1, donc SQL Server doit opter pour le tri des données. Cela peut être vu dans le plan de la requête 1 illustré à la figure 1.

Figure 1 :Planifier la requête 1 sans index de prise en charge

Notez que le plan analyse les données de l'index clusterisé avec une propriété Ordered:False. Cela signifie que l'analyse n'a pas besoin de renvoyer les lignes ordonnées par la clé d'index. C'est le cas puisque l'index clusterisé est utilisé ici simplement parce qu'il couvre la requête et non à cause de son ordre de clé. Le plan applique ensuite un tri, ce qui entraîne des coûts supplémentaires, une mise à l'échelle N Log N et un temps de réponse retardé. L'opérateur Segment produit un indicateur indiquant si la ligne est la première de la partition ou non. Enfin, l'opérateur Sequence Project attribue des numéros de ligne commençant par 1 dans chaque partition.

Si vous souhaitez éviter d'avoir à trier, vous pouvez préparer un index de couverture avec une liste de clés basée sur les éléments de partitionnement et de classement, et une liste d'inclusion basée sur les éléments de couverture. J'aime penser à cet index comme un index POC (pour le partitionnement , commander et couvrant ). Voici la définition du POC qui prend en charge notre requête :

CREATE INDEX idx_grp_data_i_id ON dbo.T1(grp, datacol) INCLUDE(id);

Exécutez à nouveau la requête 1 :

SELECT id, grp, datacol,
  ROW_NUMBER() OVER(PARTITION BY grp ORDER BY datacol) AS n 
FROM dbo.T1;

Le plan de cette exécution est illustré à la figure 2.

Figure 2 :Planifier la requête 1 avec un index POC

Observez que cette fois le plan analyse l'index POC avec une propriété Ordered:True. Cela signifie que l'analyse garantit que les lignes seront renvoyées dans l'ordre des clés d'index. Étant donné que les données sont extraites de l'index de manière pré-ordonnée, comme la fonction de fenêtre en a besoin, il n'est pas nécessaire de procéder à un tri explicite. La mise à l'échelle de ce plan est linéaire et le temps de réponse est bon.

Lorsque la commande n'a pas d'importance

Les choses deviennent un peu délicates lorsque vous devez attribuer des numéros de ligne avec un ordre complètement non déterministe. La chose naturelle à faire dans un tel cas est d'utiliser la fonction ROW_NUMBER sans spécifier de clause d'ordre de fenêtre. Tout d'abord, vérifions si le standard SQL le permet. Voici la partie pertinente de la norme définissant les règles de syntaxe pour les fonctions de fenêtre :

Règles de syntaxe

5) Soit WNS le . Soit WDX un descripteur de structure de fenêtre qui décrit la fenêtre définie par WNS.

6) Si , , ou ROW_NUMBER est spécifié, alors :

a) Si , , RANK ou DENSE_RANK est spécifié, alors la clause d'ordre de fenêtre WOC de WDX doit être présente.

f) ROW_NUMBER() OVER WNS est équivalent à la  :COUNT (*) OVER (WNS1 ROWS UNBOUNDED PRECEDING)

Notez que l'élément 6 répertorie les fonctions , , ou ROW_NUMBER, puis l'élément 6a indique que pour les fonctions , , RANK ou DENSE_RANK la clause d'ordre de fenêtre doit être présente. Il n'y a pas de langage explicite indiquant si ROW_NUMBER nécessite ou non une clause d'ordre de fenêtre, mais la mention de la fonction dans l'élément 6 et son omission dans 6a pourraient impliquer que la clause est facultative pour cette fonction. Il est assez évident que des fonctions comme RANK et DENSE_RANK nécessiteraient une clause d'ordre de fenêtre, puisque ces fonctions se spécialisent dans la gestion des liens, et les liens n'existent que lorsqu'il existe une spécification de commande. Cependant, vous pouvez certainement voir comment la fonction ROW_NUMBER pourrait bénéficier d'une clause d'ordre de fenêtre facultative.

Alors, essayons et essayons de calculer les numéros de ligne sans ordre de fenêtre dans SQL Server :

SELECT id, grp, datacol,
  ROW_NUMBER() OVER() AS n 
FROM dbo.T1;

Cette tentative génère l'erreur suivante :

Msg 4112, Niveau 15, État 1, Ligne 53
La fonction 'ROW_NUMBER' doit avoir une clause OVER avec ORDER BY.

En effet, si vous consultez la documentation de SQL Server sur la fonction ROW_NUMBER, vous trouverez le texte suivant :

"order_by_clause

La clause ORDER BY détermine l'ordre dans lequel les lignes reçoivent leur ROW_NUMBER unique dans une partition spécifiée. C'est obligatoire."

Donc, apparemment, la clause d'ordre des fenêtres est obligatoire pour la fonction ROW_NUMBER dans SQL Server. C'est également le cas dans Oracle, soit dit en passant.

Je dois dire que je ne suis pas sûr de comprendre le raisonnement derrière cette exigence. N'oubliez pas que vous autorisez la définition des numéros de ligne en fonction d'un ordre partiellement non déterministe, comme dans la requête 1. Alors pourquoi ne pas autoriser le non déterminisme jusqu'au bout ? Il y a peut-être une raison à laquelle je ne pense pas. Si vous pensez à une telle raison, merci de la partager.

Dans tous les cas, vous pourriez dire que si vous ne vous souciez pas de la commande, étant donné que la clause de commande de fenêtre est obligatoire, vous pouvez spécifier n'importe quelle commande. Le problème avec cette approche est que si vous commandez par une colonne de la ou des tables interrogées, cela pourrait impliquer une pénalité de performance inutile. Lorsqu'il n'y a pas d'index de support en place, vous paierez pour le tri explicite. Lorsqu'un index de prise en charge est en place, vous limitez le moteur de stockage à une stratégie d'analyse de l'ordre des index (en suivant la liste liée de l'index). Vous ne lui accordez pas plus de flexibilité comme c'est généralement le cas lorsque l'ordre n'a pas d'importance en choisissant entre une analyse d'ordre d'index et une analyse d'ordre d'allocation (basée sur les pages IAM).

Une idée qui vaut la peine d'être essayée est de spécifier une constante, comme 1, dans la clause d'ordre de la fenêtre. S'il est pris en charge, vous espérez que l'optimiseur est suffisamment intelligent pour se rendre compte que toutes les lignes ont la même valeur, il n'y a donc pas de réelle pertinence de classement et donc pas besoin de forcer un tri ou une analyse de l'ordre d'index. Voici une requête qui tente cette approche :

SELECT id, grp, datacol,
  ROW_NUMBER() OVER(ORDER BY 1) AS n 
FROM dbo.T1;

Malheureusement, SQL Server ne prend pas en charge cette solution. Il génère l'erreur suivante :

Msg 5308, niveau 16, état 1, ligne 56
Les fonctions fenêtrées, les agrégats et les fonctions NEXT VALUE FOR ne prennent pas en charge les indices entiers en tant qu'expressions de la clause ORDER BY.

Apparemment, SQL Server suppose que si vous utilisez une constante entière dans la clause d'ordre de la fenêtre, elle représente une position ordinale d'un élément dans la liste SELECT, comme lorsque vous spécifiez un entier dans la clause ORDER BY de présentation. Si tel est le cas, une autre option qui vaut la peine d'être essayée consiste à spécifier une constante non entière, comme ceci :

SELECT id, grp, datacol,
  ROW_NUMBER() OVER(ORDER BY 'No Order') AS n 
FROM dbo.T1;

Il s'avère que cette solution n'est pas non plus prise en charge. SQL Server génère l'erreur suivante :

Msg 5309, Niveau 16, État 1, Ligne 65
Les fonctions fenêtrées, les agrégats et les fonctions NEXT VALUE FOR ne prennent pas en charge les constantes en tant qu'expressions de la clause ORDER BY.

Apparemment, la clause d'ordre des fenêtres ne prend en charge aucun type de constante.

Jusqu'à présent, nous avons appris ce qui suit sur la pertinence de l'ordre des fenêtres de la fonction ROW_NUMBER dans SQL Server :

  1. ORDER BY est obligatoire.
  2. Impossible de trier par une constante entière puisque SQL Server pense que vous essayez de spécifier une position ordinale dans le SELECT.
  3. Impossible de trier par n'importe quel type de constante.

La conclusion est que vous êtes censé ordonner par des expressions qui ne sont pas des constantes. Évidemment, vous pouvez trier par une liste de colonnes à partir des tables interrogées. Mais nous sommes à la recherche d'une solution efficace où l'optimiseur peut se rendre compte qu'il n'y a aucune pertinence de commande.

Pliage constant

La conclusion jusqu'à présent est que vous ne pouvez pas utiliser de constantes dans la clause d'ordre de fenêtre du ROW_NUMBER, mais qu'en est-il des expressions basées sur des constantes, comme dans la requête suivante :

SELECT id, grp, datacol,
  ROW_NUMBER() OVER(ORDER BY 1+0) AS n 
FROM dbo.T1;

Cependant, cette tentative est victime d'un processus connu sous le nom de pliage constant, qui a normalement un impact positif sur les performances des requêtes. L'idée derrière cette technique est d'améliorer les performances des requêtes en pliant certaines expressions basées sur des constantes à leurs constantes de résultat à un stade précoce du traitement de la requête. Vous pouvez trouver des détails sur les types d'expressions qui peuvent être pliées en permanence ici. Notre expression 1+0 est repliée sur 1, ce qui entraîne la même erreur que celle que vous avez obtenue en spécifiant directement la constante 1 :

Msg 5308, Niveau 16, État 1, Ligne 79
Les fonctions fenêtrées, les agrégats et les fonctions NEXT VALUE FOR ne prennent pas en charge les indices entiers en tant qu'expressions de la clause ORDER BY.

Vous feriez face à une situation similaire lorsque vous tenteriez de concaténer deux littéraux de chaîne de caractères, comme ceci :

SELECT id, grp, datacol,
  ROW_NUMBER() OVER(ORDER BY 'No' + ' Order') AS n 
FROM dbo.T1;

Vous obtenez la même erreur que lorsque vous spécifiez directement le littéral "Pas de commande" :

Msg 5309, Niveau 16, État 1, Ligne 55
Les fonctions fenêtrées, les agrégats et les fonctions NEXT VALUE FOR ne prennent pas en charge les constantes en tant qu'expressions de la clause ORDER BY.

Monde Bizarro - les erreurs qui évitent les erreurs

La vie est pleine de surprises…

Une chose qui empêche le pliage constant est le moment où l'expression entraînerait normalement une erreur. Par exemple, l'expression 2147483646+1 peut être constante repliée puisqu'elle donne une valeur de type INT valide. Par conséquent, une tentative d'exécution de la requête suivante échoue :

SELECT id, grp, datacol,
  ROW_NUMBER() OVER(ORDER BY 2147483646+1) AS n 
FROM dbo.T1;
Msg 5308, niveau 16, état 1, ligne 109
Les fonctions fenêtrées, les agrégats et les fonctions NEXT VALUE FOR ne prennent pas en charge les indices entiers en tant qu'expressions de la clause ORDER BY.

Cependant, l'expression 2147483647+1 ne peut pas être pliée en permanence car une telle tentative aurait entraîné une erreur de dépassement INT. L'implication sur la commande est assez intéressante. Essayez la requête suivante (nous l'appellerons requête 2) :

SELECT id, grp, datacol,
  ROW_NUMBER() OVER(ORDER BY 2147483647+1) AS n 
FROM dbo.T1;

Bizarrement, cette requête s'exécute avec succès ! Ce qui se passe, c'est que, d'une part, SQL Server ne parvient pas à appliquer le pliage constant et, par conséquent, l'ordre est basé sur une expression qui n'est pas une constante unique. D'un autre côté, l'optimiseur estime que la valeur de tri est la même pour toutes les lignes, il ignore donc complètement l'expression de tri. Ceci est confirmé lors de l'examen du plan de cette requête, comme illustré à la figure 3.

Figure 3 :Plan pour la requête 2

Observez que le plan analyse certains index de couverture avec une propriété Ordered:False. C'était exactement notre objectif de performance.

De la même manière, la requête suivante implique une tentative réussie de pliage constant, et échoue donc :

SELECT id, grp, datacol,
  ROW_NUMBER() OVER(ORDER BY 1/1) AS n 
FROM dbo.T1;
Msg 5308, Niveau 16, État 1, Ligne 123
Les fonctions fenêtrées, les agrégats et les fonctions NEXT VALUE FOR ne prennent pas en charge les indices entiers en tant qu'expressions de la clause ORDER BY.

La requête suivante implique une tentative échouée de pliage constant, et réussit donc, générant le plan présenté précédemment dans la figure 3 :

SELECT id, grp, datacol,
  ROW_NUMBER() OVER(ORDER BY 1/0) AS n 
FROM dbo.T1;

La requête suivante implique une tentative réussie de pliage constant (le littéral VARCHAR '1' est implicitement converti en INT 1, puis 1 + 1 est plié en 2), et échoue donc :

SELECT id, grp, datacol,
  ROW_NUMBER() OVER(ORDER BY 1+'1') AS n 
FROM dbo.T1;
Msg 5308, Niveau 16, État 1, Ligne 134
Les fonctions fenêtrées, les agrégats et les fonctions NEXT VALUE FOR ne prennent pas en charge les indices entiers en tant qu'expressions de la clause ORDER BY.

La requête suivante implique un échec de tentative de pliage constant (impossible de convertir « A » en INT), et réussit donc, générant le plan illustré précédemment dans la figure 3 :

SELECT id, grp, datacol,
  ROW_NUMBER() OVER(ORDER BY 1+'A') AS n 
FROM dbo.T1;

Pour être honnête, même si cette technique bizarre atteint notre objectif de performance initial, je ne peux pas dire que je la considère comme sûre et donc je ne suis pas si à l'aise de m'y fier.

Constantes d'exécution basées sur des fonctions

Poursuivant la recherche d'une bonne solution pour calculer les numéros de lignes avec un ordre non déterministe, il existe quelques techniques qui semblent plus sûres que la dernière solution originale :utiliser des constantes d'exécution basées sur des fonctions, utiliser une sous-requête basée sur une constante, utiliser une colonne aliasée basée sur une constante et en utilisant une variable.

Comme je l'explique dans Bugs, pièges et meilleures pratiques de T-SQL - déterminisme, la plupart des fonctions de T-SQL ne sont évaluées qu'une seule fois par référence dans la requête, et non une fois par ligne. C'est le cas même avec la plupart des fonctions non déterministes comme GETDATE et RAND. Il y a très peu d'exceptions à cette règle, comme les fonctions NEWID et CRYPT_GEN_RANDOM, qui sont évaluées une fois par ligne. La plupart des fonctions, telles que GETDATE, @@SPID et bien d'autres, sont évaluées une fois au début de la requête, et leurs valeurs sont alors considérées comme des constantes d'exécution. Une référence à de telles fonctions n'est pas constamment repliée. Ces caractéristiques font d'une constante d'exécution basée sur une fonction un bon choix comme élément de commande de fenêtre, et en effet, il semble que T-SQL le supporte. Dans le même temps, l'optimiseur se rend compte qu'en pratique, il n'y a pas de pertinence de commande, ce qui évite des pénalités de performances inutiles.

Voici un exemple utilisant la fonction GETDATE :

SELECT id, grp, datacol,
  ROW_NUMBER() OVER(ORDER BY GETDATE()) AS n 
FROM dbo.T1;

Cette requête obtient le même plan que celui illustré précédemment dans la figure 3.

Voici un autre exemple utilisant la fonction @@SPID (renvoyant l'ID de session en cours) :

SELECT id, grp, datacol,
  ROW_NUMBER() OVER(ORDER BY @@SPID) AS n 
FROM dbo.T1;

Qu'en est-il de la fonction PI ? Essayez la requête suivante :

SELECT id, grp, datacol,
  ROW_NUMBER() OVER(ORDER BY PI()) AS n 
FROM dbo.T1;

Celui-ci échoue avec l'erreur suivante :

Msg 5309, Niveau 16, État 1, Ligne 153
Les fonctions fenêtrées, les agrégats et les fonctions NEXT VALUE FOR ne prennent pas en charge les constantes en tant qu'expressions de la clause ORDER BY.

Des fonctions telles que GETDATE et @@SPID sont réévaluées une fois par exécution du plan, de sorte qu'elles ne peuvent pas être pliées en permanence. PI représente toujours la même constante et devient donc constante pliée.

Comme mentionné précédemment, très peu de fonctions sont évaluées une fois par ligne, telles que NEWID et CRYPT_GEN_RANDOM. Cela en fait un mauvais choix en tant qu'élément de commande de fenêtre si vous avez besoin d'un ordre non déterministe, à ne pas confondre avec un ordre aléatoire. Pourquoi payer une pénalité de tri inutile ?

Voici un exemple utilisant la fonction NEWID :

SELECT id, grp, datacol,
  ROW_NUMBER() OVER(ORDER BY NEWID()) AS n 
FROM dbo.T1;

Le plan de cette requête est illustré à la figure 4, confirmant que SQL Server a ajouté un tri explicite basé sur le résultat de la fonction.

Figure 4 :Plan pour la requête 3

Si vous voulez que les numéros de ligne soient attribués dans un ordre aléatoire, c'est la technique que vous souhaitez utiliser. Vous devez juste être conscient que cela entraîne des frais de tri.

Utiliser une sous-requête

Vous pouvez également utiliser une sous-requête basée sur une constante comme expression d'ordre de fenêtre (par exemple, ORDER BY (SELECT 'No Order')). De plus, avec cette solution, l'optimiseur de SQL Server reconnaît qu'il n'y a pas de pertinence d'ordre, et n'impose donc pas un tri inutile ou ne limite pas les choix du moteur de stockage à ceux qui doivent garantir l'ordre. Essayez d'exécuter la requête suivante à titre d'exemple :

SELECT id, grp, datacol,
  ROW_NUMBER() OVER(ORDER BY (SELECT 'No Order')) AS n 
FROM dbo.T1;

Vous obtenez le même plan que celui illustré précédemment dans la figure 3.

L'un des grands avantages de cette technique est que vous pouvez ajouter votre touche personnelle. Peut-être que vous aimez vraiment les NULL :

SELECT id, grp, datacol,
  ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS n 
FROM dbo.T1;

Peut-être que vous aimez vraiment un certain nombre :

SELECT id, grp, datacol,
  ROW_NUMBER() OVER(ORDER BY (SELECT 42)) AS n 
FROM dbo.T1;

Peut-être voulez-vous envoyer un message à quelqu'un :

SELECT id, grp, datacol,
  ROW_NUMBER() OVER(ORDER BY (SELECT 'Lilach, will you marry me?')) AS n 
FROM dbo.T1;

Vous avez compris.

Faisable, mais gênant

Il existe quelques techniques qui fonctionnent, mais qui sont un peu maladroites. L'une consiste à définir un alias de colonne pour une expression basée sur une constante, puis à utiliser cet alias de colonne comme élément de commande de fenêtre. Vous pouvez le faire en utilisant une expression de table ou avec l'opérateur CROSS APPLY et un constructeur de valeur de table. Voici un exemple pour ce dernier :

SELECT id, grp, datacol,
  ROW_NUMBER() OVER(ORDER BY [I'm a bit ugly]) AS n 
FROM dbo.T1 CROSS APPLY ( VALUES('No Order') ) AS A([I'm a bit ugly]);

Vous obtenez le même plan que celui illustré précédemment dans la figure 3.

Une autre option consiste à utiliser une variable comme élément de commande de fenêtre :

DECLARE @ImABitUglyToo AS INT = NULL;
 
SELECT id, grp, datacol,
  ROW_NUMBER() OVER(ORDER BY @ImABitUglyToo) AS n 
FROM dbo.T1;

Cette requête obtient également le plan illustré précédemment dans la figure 3.

Et si j'utilise mon propre UDF ?

Vous pourriez penser que l'utilisation de votre propre UDF qui renvoie une constante pourrait être un bon choix comme élément de commande de fenêtre lorsque vous voulez un ordre non déterministe, mais ce n'est pas le cas. Considérez la définition UDF suivante comme exemple :

DROP FUNCTION IF EXISTS dbo.YouWillRegretThis;
GO
 
CREATE FUNCTION dbo.YouWillRegretThis() RETURNS INT
AS
BEGIN
  RETURN NULL
END;
GO

Essayez d'utiliser l'UDF comme clause d'ordre des fenêtres, comme ceci (nous appellerons celui-ci Requête 4) :

SELECT id, grp, datacol,
  ROW_NUMBER() OVER(ORDER BY dbo.YouWillRegretThis()) AS n 
FROM dbo.T1;

Avant SQL Server 2019 (ou niveau de compatibilité parallèle <150), les fonctions définies par l'utilisateur sont évaluées par ligne. Même s'ils renvoient une constante, ils ne sont pas alignés. Par conséquent, d'une part, vous pouvez utiliser une telle UDF comme élément de commande de fenêtre, mais d'autre part, cela entraîne une pénalité de tri. Ceci est confirmé en examinant le plan de cette requête, comme illustré à la figure 5.

Figure 5 :Plan pour la requête 4

À partir de SQL Server 2019, sous le niveau de compatibilité>=150, ces fonctions définies par l'utilisateur sont intégrées, ce qui est généralement une bonne chose, mais dans notre cas, cela entraîne une erreur :

Msg 5309, Niveau 16, État 1, Ligne 217
Les fonctions fenêtrées, les agrégats et les fonctions NEXT VALUE FOR ne prennent pas en charge les constantes en tant qu'expressions de la clause ORDER BY.

Ainsi, l'utilisation d'une UDF basée sur une constante comme élément de commande de fenêtre force un tri ou une erreur en fonction de la version de SQL Server que vous utilisez et du niveau de compatibilité de votre base de données. Bref, ne faites pas ça.

Numéros de ligne partitionnés avec un ordre non déterministe

Un cas d'utilisation courant pour les numéros de ligne partitionnés basés sur un ordre non déterministe renvoie n'importe quelle ligne par groupe. Étant donné que, par définition, un élément de partitionnement existe dans ce scénario, vous penseriez qu'une technique sûre dans un tel cas serait d'utiliser l'élément de partitionnement de fenêtre également comme élément de commande de fenêtre. Dans un premier temps, vous calculez les numéros de ligne comme suit :

SELECT id, grp, datacol,
  ROW_NUMBER() OVER(PARTITION BY grp ORDER BY grp) AS n 
FROM dbo.T1;

Le plan de cette requête est illustré à la figure 6.

Figure 6 :Plan pour la requête 5

La raison pour laquelle notre index de prise en charge est analysé avec une propriété Ordered:True est que SQL Server doit traiter les lignes de chaque partition comme une seule unité. C'est le cas avant le filtrage. Si vous ne filtrez qu'une seule ligne par partition, vous disposez à la fois d'algorithmes basés sur l'ordre et basés sur le hachage.

La deuxième étape consiste à placer la requête avec le calcul du numéro de ligne dans une expression de table et, dans la requête externe, à filtrer la ligne avec le numéro de ligne 1 dans chaque partition, comme ceci :

WITH C AS
(
  SELECT id, grp, datacol,
    ROW_NUMBER() OVER(PARTITION BY grp ORDER BY grp) AS n 
  FROM dbo.T1
)
SELECT id, grp, datacol
FROM C
WHERE n = 1;

Théoriquement, cette technique est censée être sûre, mais Paul White a trouvé un bogue qui montre qu'en utilisant cette méthode, vous pouvez obtenir des attributs de différentes lignes source dans la ligne de résultat renvoyée par partition. Utiliser une constante d'exécution basée sur une fonction ou une sous-requête basée sur une constante car l'élément de commande semble être sûr même avec ce scénario, alors assurez-vous d'utiliser plutôt une solution telle que la suivante :

WITH C AS
(
  SELECT id, grp, datacol,
    ROW_NUMBER() OVER(PARTITION BY grp ORDER BY (SELECT 'No Order')) AS n 
  FROM dbo.T1
)
SELECT id, grp, datacol
FROM C
WHERE n = 1;

Personne ne doit passer par là sans ma permission

Essayer de calculer les numéros de lignes en fonction d'un ordre non déterministe est un besoin courant. Cela aurait été bien si T-SQL rendait simplement la clause d'ordre des fenêtres facultative pour la fonction ROW_NUMBER, mais ce n'est pas le cas. Sinon, cela aurait été bien s'il permettait au moins d'utiliser une constante comme élément de commande, mais ce n'est pas non plus une option prise en charge. Mais si vous le demandez gentiment, sous la forme d'une sous-requête basée sur une constante ou d'une constante d'exécution basée sur une fonction, SQL Server le permettra. Ce sont les deux options avec lesquelles je suis le plus à l'aise. Je ne me sens pas vraiment à l'aise avec les expressions erronées bizarres qui semblent fonctionner, donc je ne peux pas recommander cette option.