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

Défi T-SQL des îles

Récemment, mon ami Erland Sommarskog m'a présenté un nouveau défi d'îles. Il est basé sur une question d'un forum de base de données publique. Le défi lui-même n'est pas compliqué à gérer lorsque vous utilisez des techniques bien connues, qui utilisent principalement des fonctions de fenêtre. Cependant, ces techniques nécessitent un tri explicite malgré la présence d'un index de support. Cela affecte l'évolutivité et le temps de réponse des solutions. Passionné de défis, j'ai cherché une solution pour minimiser le nombre d'opérateurs de tri explicites dans le plan, ou mieux encore, éliminer complètement le besoin de ceux-ci. Et j'ai trouvé une telle solution.

Je commencerai par présenter une forme généralisée du défi. Je montrerai ensuite deux solutions basées sur des techniques connues, suivies de la nouvelle solution. Enfin, je comparerai les performances des différentes solutions.

Je vous recommande d'essayer de trouver une solution avant d'implémenter la mienne.

Le défi

Je vais présenter une forme généralisée du défi original des îles d'Erland.

Utilisez le code suivant pour créer une table appelée T1 et la remplir avec un petit ensemble d'exemples de données :

SET NOCOUNT ON ; UTILISER tempdb ; DROP TABLE IF EXISTS dbo.T1 CREATE TABLE dbo.T1( grp VARCHAR(10) NOT NULL, ord INT NOT NULL, val VARCHAR(10) NOT NULL, CONTRAINTE PK_T1 PRIMARY KEY(grp, ord));GO INSERT INTO dbo. T1(grp, ord, val) VALEURS ('Groupe A', 1002, 'Y'), ('Groupe A', 1003, 'Y'), ('Groupe A', 1005, 'Y'), (' Groupe A', 1007, 'N'), ('Groupe A', 1011, 'N'), ('Groupe A', 1013, 'N'), ('Groupe A', 1017, 'Y'), ('Groupe A', 1019, 'Y'), ('Groupe A', 1023, 'N'), ('Groupe A', 1029, 'N'), ('Groupe B', 1001, 'X' ), ('Groupe B', 1002, 'X'), ('Groupe B', 1003, 'Z'), ('Groupe B', 1005, 'Z'), ('Groupe B', 1008, ' Z'), ('Groupe B', 1013, 'Z'), ('Groupe B', 1021, 'Y'), ('Groupe B', 1034, 'Y');

Le défi est le suivant :

En supposant un partitionnement basé sur la colonne grp et un classement basé sur la colonne ord, calculez les numéros de ligne séquentiels commençant par 1 dans chaque groupe consécutif de lignes avec la même valeur dans la colonne val. Voici le résultat souhaité pour le petit ensemble d'exemples de données :

grp ord val seqno-------- ----- ---- ------Groupe A 1002 Y 1Groupe A 1003 Y 2Groupe A 1005 Y 3Groupe A 1007 N 1Groupe A 1011 N 2Groupe A 1013 N 3Groupe A 1017 Y 1Groupe A 1019 Y 2Groupe A 1023 N 1Groupe A 1029 N 2Groupe B 1001 X 1Groupe B 1002 X 2Groupe B 1003 Z 1Groupe B 1005 Z 2Groupe B 1008 Z 3Groupe B 1013 Z 4Groupe B 1021 Y 1Groupe B 1034 Y 2

Notez la définition de la contrainte de clé primaire basée sur la clé composite (grp, ord), qui se traduit par un index clusterisé basé sur la même clé. Cet index peut potentiellement prendre en charge les fonctions de fenêtre partitionnées par grp et ordonnées par ord.

Pour tester les performances de votre solution, vous devrez remplir le tableau avec de plus grands volumes d'exemples de données. Utilisez le code suivant pour créer la fonction d'assistance GetNums :

CREATE FUNCTION dbo.GetNums(@low AS BIGINT =1, @high AS BIGINT) RENVOIE TABLEASRETURN WITH L0 AS ( SELECT 1 AS c FROM (VALUES(1),(1),(1),(1), (1),(1),(1),(1), (1),(1),(1),(1),(1),(1),(1),(1)) AS D (c) ), L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ), L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ), L3 AS ( SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B ), Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum FROM L3 ) SELECT TOP(@high - @low + 1) rownum AS rn, @high + 1 - rownum AS op, @low - 1 + rownum AS n FROM Nums ORDER BY rownum;GO

Utilisez le code suivant pour renseigner T1, après avoir modifié les paramètres représentant le nombre de groupes, le nombre de lignes par groupe et le nombre de valeurs distinctes comme vous le souhaitez :

DECLARE @numgroups AS INT =1000, @rowspergroup AS INT =10000, -- test avec 1000 à 10000 ici @uniquevalues ​​AS INT =5 ; ALTER TABLE dbo.T1 DROP CONTRAINTE PK_T1 ; TRUNCATE TABLE dbo.T1 ; INSERT INTO dbo.T1 WITH(TABLOCK) (grp, ord, val) SELECT CAST(G.n AS VARCHAR(10)) AS grp, CAST(R.n AS INT) AS ord, CAST(ABS(CHECKSUM(NEWID())) % @uniquevalues ​​+ 1 AS VARCHAR(10)) AS val FROM dbo.GetNums(1, @numgroups) AS G CROSS JOIN dbo.GetNums(1, @rowspergroup) AS R ; ALTER TABLE dbo.T1 AJOUTER LA CONTRAINTE PK_T1 PRIMARY KEY CLUSTERED(grp, ord);

Lors de mes tests de performances, j'ai rempli le tableau avec 1 000 groupes, entre 1 000 et 10 000 lignes par groupe (donc 1M à 10M lignes), et 5 valeurs distinctes. J'ai utilisé un SELECT INTO pour écrire le résultat dans une table temporaire.

Ma machine de test dispose de quatre processeurs logiques, exécutant SQL Server 2019 Enterprise.

Je suppose que vous utilisez un environnement conçu pour prendre en charge le mode batch sur le magasin de lignes, soit directement, par exemple, en utilisant SQL Server 2019 Enterprise Edition comme le mien, soit indirectement, en créant un index columnstore factice sur la table.

N'oubliez pas, des points supplémentaires si vous parvenez à trouver une solution efficace sans tri explicite dans le plan. Bonne chance !

Un opérateur de tri est-il nécessaire pour optimiser les fonctions de la fenêtre ?

Avant de couvrir les solutions, un peu de contexte d'optimisation afin que ce que vous verrez dans les plans de requête plus tard ait plus de sens.

Les techniques les plus courantes pour résoudre des tâches d'îles telles que la nôtre impliquent l'utilisation d'une combinaison de fonctions de fenêtre d'agrégation et/ou de classement. SQL Server peut traiter ces fonctions de fenêtre à l'aide d'une série d'anciens opérateurs en mode ligne (Segment, Sequence Project, Segment, Window Spool, Stream Aggregate) ou de l'opérateur Window Aggregate en mode batch plus récent et généralement plus efficace.

Dans les deux cas, les opérateurs gérant le calcul de la fonction de fenêtre doivent ingérer les données ordonnées par les éléments de partitionnement et d'ordonnancement de la fenêtre. Si vous n'avez pas d'index de prise en charge, SQL Server devra naturellement introduire un opérateur de tri dans le plan. Par exemple, si vous avez plusieurs fonctions de fenêtre dans votre solution avec plus d'une combinaison unique de partitionnement et de classement, vous êtes obligé d'avoir un tri explicite dans le plan. Mais que se passe-t-il si vous n'avez qu'une seule combinaison unique de partitionnement et de classement et un index de support ?

L'ancienne méthode en mode ligne peut s'appuyer sur des données préordonnées ingérées à partir d'un index sans avoir besoin d'un opérateur de tri explicite dans les modes série et parallèle. Le nouvel opérateur en mode batch élimine une grande partie des inefficacités de l'ancienne optimisation en mode ligne et présente les avantages inhérents au traitement en mode batch. Cependant, son implémentation parallèle actuelle nécessite un opérateur de tri parallèle en mode batch intermédiaire même lorsqu'un index de prise en charge est présent. Seule son implémentation en série peut s'appuyer sur l'ordre de l'index sans opérateur de tri. Tout cela pour dire que lorsque l'optimiseur doit choisir une stratégie pour gérer votre fonction de fenêtre, en supposant que vous ayez un index de support, ce sera généralement l'une des quatre options suivantes :

  1. Mode ligne, série, pas de tri
  2. Mode ligne, parallèle, pas de tri
  3. Mode batch, série, pas de tri
  4. Mode batch, parallèle, tri

Celui qui aboutit au coût de plan le plus bas sera choisi, en supposant bien sûr que les conditions préalables pour le parallélisme et le mode batch sont remplies lors de leur examen. Normalement, pour que l'optimiseur justifie un plan parallèle, les avantages du parallélisme doivent l'emporter sur le travail supplémentaire comme la synchronisation des threads. Avec l'option 4 ci-dessus, les avantages du parallélisme doivent l'emporter sur le travail supplémentaire habituel impliqué par le parallélisme, plus le tri supplémentaire.

Tout en expérimentant différentes solutions à notre défi, j'ai eu des cas où l'optimiseur a choisi l'option 2 ci-dessus. Il l'a choisi malgré le fait que la méthode en mode ligne comporte quelques inefficacités car les avantages du parallélisme et de l'absence de tri se traduisent par un plan à moindre coût que les alternatives. Dans certains de ces cas, forcer un plan en série avec un indice a entraîné l'option 3 ci-dessus et de meilleures performances.

Avec ce contexte à l'esprit, examinons les solutions. Je commencerai par deux solutions s'appuyant sur des techniques connues pour les tâches d'îlots qui ne peuvent pas échapper au tri explicite dans le plan.

Solution basée sur la technique connue 1

Voici la première solution à notre défi, qui repose sur une technique connue depuis un certain temps :

WITH C AS( SELECT *, ROW_NUMBER() OVER(PARTITION BY grp ORDER BY ord) - ROW_NUMBER() OVER(PARTITION BY grp, val ORDER BY ord) AS island FROM dbo.T1)SELECT grp, ord, val , ROW_NUMBER() OVER(PARTITION BY grp, val, island ORDER BY ord) AS seqnoFROM C;

Je l'appellerai Solution connue 1.

Le CTE appelé C est basé sur une requête qui calcule deux numéros de ligne. Le premier (je l'appellerai rn1) est partitionné par grp et ordonné par ord. Le second (je l'appellerai rn2) est partitionné par grp et val et ordonné par ord. Étant donné que rn1 a des écarts entre différents groupes de la même valeur et que rn2 n'en a pas, la différence entre rn1 et rn2 (colonne appelée île) est une valeur constante unique pour toutes les lignes avec les mêmes valeurs grp et val. Voici le résultat de la requête interne, y compris les résultats des calculs de nombre à deux lignes, qui ne sont pas renvoyés sous forme de colonnes séparées :

grp ord val rn1 rn2 island-------- ----- ---- ---- ---- -------Groupe A 1002 Y 1 1 0Groupe A 1003 O 2 2 0Groupe A 1005 O 3 3 0Groupe A 1007 N 4 1 3Groupe A 1011 N 5 2 3Groupe A 1013 N 6 3 3Groupe A 1017 O 7 4 3Groupe A 1019 O 8 5 3Groupe A 1023 N 9 4 5Groupe A 1029 N 10 5 5Groupe B 1001 X 1 1 0Groupe B 1002 X 2 2 0Groupe B 1003 Z 3 1 2Groupe B 1005 Z 4 2 2Groupe B 1008 Z 5 3 2Groupe B 1013 Z 6 4 2Groupe B 1021 Y 7 1 6Groupe B 1034 Y 8 2 6 

Ce qu'il reste à faire pour la requête externe est de calculer la colonne de résultat seqno à l'aide de la fonction ROW_NUMBER, partitionnée par grp, val et island, et triée par ord, générant le résultat souhaité.

Notez que vous pouvez obtenir la même valeur d'îlot pour différentes valeurs val dans la même partition, comme dans la sortie ci-dessus. C'est pourquoi il est important d'utiliser grp, val et island comme éléments de partitionnement de fenêtre et non grp et island seuls.

De même, si vous avez affaire à une tâche d'îlots qui vous oblige à regrouper les données par îlot et à calculer des agrégats de groupe, vous regrouperez les lignes par grp, val et island. Mais ce n'est pas le cas avec notre défi. Ici, vous avez été chargé de simplement calculer les numéros de ligne indépendamment pour chaque île.

La figure 1 présente le plan par défaut que j'ai obtenu pour cette solution sur ma machine après avoir rempli le tableau avec 10 millions de lignes.

Figure 1 :plan parallèle pour la solution connue 1

Le calcul de rn1 peut reposer sur l'ordre des index. Ainsi, l'optimiseur a choisi la stratégie sans tri + mode ligne parallèle pour celui-ci (premiers opérateurs Segment et Sequence Project dans le plan). Étant donné que les calculs de rn2 et de seqno utilisent leurs propres combinaisons uniques d'éléments de partitionnement et d'ordonnancement, le tri explicite est inévitable pour ceux-ci, quelle que soit la stratégie utilisée. Ainsi, l'optimiseur a choisi la stratégie tri + mode batch parallèle pour les deux. Ce plan implique deux opérateurs de tri explicites.

Dans mon test de performances, il a fallu 3,68 secondes à cette solution pour 1 million de lignes et 43,1 secondes pour 10 millions de lignes.

Comme mentionné, j'ai également testé toutes les solutions en forçant un plan série (avec un indice MAXDOP 1). Le plan de série de cette solution est illustré à la figure 1.

Figure 2 :Plan de série pour la solution connue 1

Comme prévu, cette fois aussi le calcul de rn1 utilise la stratégie du mode batch sans opérateur de tri précédent, mais le plan a toujours deux opérateurs de tri pour les calculs de numéro de ligne suivants. Le plan série a été moins performant que le plan parallèle sur ma machine avec toutes les tailles d'entrée que j'ai testées, prenant 4,54 secondes pour terminer avec 1 million de lignes et 61,5 secondes avec 10 millions de lignes.

Solution basée sur la technique connue 2

La deuxième solution que je vais présenter est basée sur une brillante technique de détection d'îlots que j'ai apprise de Paul White il y a quelques années. Voici le code de solution complet basé sur cette technique :

WITH C1 AS( SELECT *, CASE WHEN val =LAG(val) OVER(PARTITION BY grp ORDER BY ord) THEN 0 ELSE 1 END AS isstart FROM dbo.T1),C2 AS( SELECT *, SUM(isstart) OVER(PARTITION BY grp ORDER BY ord ROWS UNBOUNDED PRECEDING) AS island FROM C1)SELECT grp, ord, val, ROW_NUMBER() OVER(PARTITION BY grp, island ORDER BY ord) AS seqnoFROM C2 ;

Je ferai référence à cette solution sous le nom de solution connue 2.

La requête définissant les calculs CTE C1 utilise une expression CASE et la fonction de fenêtre LAG (partitionnée par grp et ordonnée par ord) pour calculer une colonne de résultat appelée isstart. Cette colonne a la valeur 0 lorsque la valeur val actuelle est la même que la précédente et 1 sinon. En d'autres termes, c'est 1 lorsque la ligne est la première d'un îlot et 0 sinon.

Voici le résultat de la requête définissant C1 :

grp ord val isstart-------- ----- ---- --------Groupe A 1002 Y 1Groupe A 1003 Y 0Groupe A 1005 Y 0Groupe A 1007 N 1Groupe A 1011 N 0Groupe A 1013 N 0Groupe A 1017 Y 1Groupe A 1019 Y 0Groupe A 1023 N 1Groupe A 1029 N 0Groupe B 1001 X 1Groupe B 1002 X 0Groupe B 1003 Z 1Groupe B 1005 Z 0Groupe B 1008 Z 0Groupe B 1013 Z 0Groupe B 1021 Y 1Groupe B 1034 Y 0

La magie en ce qui concerne la détection des îlots se produit dans le CTE C2. La requête qui le définit calcule un identifiant d'îlot à l'aide de la fonction de fenêtre SUM (également partitionnée par grp et ordonnée par ord) appliquée à la colonne isstart. La colonne de résultat avec l'identifiant de l'île est appelée île. Dans chaque partition, vous obtenez 1 pour la première île, 2 pour la deuxième île, et ainsi de suite. Ainsi, la combinaison des colonnes grp et island est un identifiant d'île, que vous pouvez utiliser dans les tâches d'îles qui impliquent un regroupement par île, le cas échéant.

Voici le résultat de la requête définissant C2 :

grp ord val isstart island-------- ----- ---- -------- -------Groupe A 1002 O 1 1Groupe A 1003 O 0 1Groupe A 1005 O 0 1Groupe A 1007 N 1 2Groupe A 1011 N 0 2Groupe A 1013 N 0 2Groupe A 1017 O 1 3Groupe A 1019 O 0 3Groupe A 1023 N 1 4Groupe A 1029 N 0 4Groupe B 1001 X 1 1Groupe B 1002 X 0 1Groupe B 1003 Z 1 2Groupe B 1005 Z 0 2Groupe B 1008 Z 0 2Groupe B 1013 Z 0 2Groupe B 1021 Y 1 3Groupe B 1034 Y 0 3

Enfin, la requête externe calcule la colonne de résultat souhaitée seqno avec une fonction ROW_NUMBER, partitionnée par grp et island, et ordonnée par ord. Notez que cette combinaison d'éléments de partitionnement et de classement est différente de celle utilisée par les fonctions de fenêtre précédentes. Alors que le calcul des deux premières fonctions de fenêtre peut potentiellement s'appuyer sur l'ordre de l'index, la dernière ne le peut pas.

La figure 3 présente le plan par défaut que j'ai obtenu pour cette solution.

Figure 3 :plan parallèle pour la solution connue 2

Comme vous pouvez le voir dans le plan, le calcul des deux premières fonctions de fenêtre utilise la stratégie sans tri + mode ligne parallèle, et le calcul de la dernière utilise la stratégie tri + mode batch parallèle.

Les temps d'exécution que j'ai obtenus pour cette solution variaient de 2,57 secondes contre 1 million de lignes à 46,2 secondes contre 10 millions de lignes.

En forçant le traitement en série, j'ai obtenu le plan illustré à la figure 4.

Figure 4 :Plan de série pour Known Solution 2

Comme prévu, cette fois, tous les calculs de fonction de fenêtre reposent sur la stratégie du mode batch. Les deux premiers sans tri préalable, et le dernier avec. Le plan parallèle et le plan en série impliquaient un opérateur de tri explicite. Le plan série a mieux fonctionné que le plan parallèle sur ma machine avec les tailles d'entrée que j'ai testées. Les temps d'exécution que j'ai obtenus pour le plan en série forcé allaient de 1,75 seconde contre 1 million de lignes à 21,7 secondes contre 10 millions de lignes.

Solution basée sur une nouvelle technique

Quand Erland a introduit ce défi dans un forum privé, les gens étaient sceptiques quant à la possibilité de le résoudre avec une requête qui avait été optimisée avec un plan sans tri explicite. C'est tout ce que j'avais besoin d'entendre pour me motiver. Alors, voici ce que j'ai trouvé :

WITH C1 AS( SELECT *, ROW_NUMBER() OVER(PARTITION BY grp ORDER BY ord) AS rn, LAG(val) OVER(PARTITION BY grp ORDER BY ord) AS prev FROM dbo.T1),C2 AS( SELECT *, MAX(CASE WHEN val =prev THEN NULL ELSE rn END) OVER(PARTITION BY grp ORDER BY ord ROWS UNBOUNDED PRECEDING) AS firstrn FROM C1)SELECT grp, ord, val, rn - firstrn + 1 AS seqnoFROM C2; 

La solution utilise trois fonctions de fenêtre :LAG, ROW_NUMBER et MAX. L'important ici est que les trois sont basés sur le partitionnement grp et l'ordre ord, qui est aligné sur l'ordre des clés d'index de prise en charge.

La requête définissant le CTE C1 utilise la fonction ROW_NUMBER pour calculer les numéros de ligne (colonne rn) et la fonction LAG pour renvoyer la valeur val précédente (colonne prev).

Voici le résultat de la requête définissant C1 :

grp ord val rn prev-------- ----- ---- --- -----Groupe A 1002 O 1 NULLGroupe A 1003 O 2 OGroupe A 1005 O 3 OGroupe A 1007 N 4 YGroupe A 1011 N 5 NGroupe A 1013 N 6 NGroupe A 1017 Y 7 NGroupe A 1019 Y 8 YGroupe A 1023 N 9 YGroupe A 1029 N 10 NGroupe B 1001 X 1 NULLGroupe B 1002 X 2 XGroupe B 1003 Z 3 XGroupe B 1005 Z 4 ZGroupe B 1008 Z 5 ZGroupe B 1013 Z 6 ZGroupe B 1021 Y 7 ZGroupe B 1034 Y 8 Y

Remarquez que lorsque val et prev sont identiques, ce n'est pas la première ligne de l'île, sinon c'est le cas.

Selon cette logique, la requête définissant le CTE C2 utilise une expression CASE qui renvoie rn lorsque la ligne est la première d'un îlot et NULL sinon. Le code applique ensuite la fonction de fenêtre MAX au résultat de l'expression CASE, renvoyant le premier rn de l'îlot (première colonne).

Voici la sortie de la requête définissant C2, y compris la sortie de l'expression CASE :

grp ord val rn prev CASE firstrn-------- ----- ---- --- ----- ----- --------Groupe A 1002 O 1 NULL 1 1Groupe A 1003 O 2 O NULL 1Groupe A 1005 O 3 O NULL 1Groupe A 1007 N 4 O 4 4Groupe A 1011 N 5 N NULL 4Groupe A 1013 N 6 N NULL 4Groupe A 1017 O 7 N 7 7Groupe A 1019 Y 8 Y NULL 7Groupe A 1023 N 9 Y 9 9Groupe A 1029 N 10 N NULL 9Groupe B 1001 X 1 NULL 1 1Groupe B 1002 X 2 X NULL 1Groupe B 1003 Z 3 X 3 3Groupe B 1005 Z 4 Z NULL 3Groupe B 1008 Z 5 Z NULL 3Groupe B 1013 Z 6 Z NULL 3Groupe B 1021 Y 7 Z 7 7Groupe B 1034 Y 8 Y NULL 7

Ce qui reste pour la requête externe est de calculer la colonne de résultat souhaitée seqno comme rn moins firstrn plus 1.

La figure 5 présente le plan parallèle par défaut que j'ai obtenu pour cette solution lors du test à l'aide d'une instruction SELECT INTO, en écrivant le résultat dans une table temporaire.

Figure 5 :Plan parallèle pour une nouvelle solution

Il n'y a pas d'opérateurs de tri explicites dans ce plan. Cependant, les trois fonctions de fenêtre sont calculées à l'aide de la stratégie sans tri + mode de ligne parallèle, nous manquons donc les avantages du traitement par lots. Les temps d'exécution que j'ai obtenus pour cette solution avec le plan parallèle variaient de 2,47 secondes contre 1 million de lignes et de 41,4 secondes contre 10 millions de lignes.

Ici, il y a de fortes chances qu'un plan en série avec traitement par lots soit nettement meilleur, en particulier lorsque la machine n'a pas beaucoup de processeurs. Rappelons que je teste mes solutions sur une machine avec 4 processeurs logiques. La figure 6 présente le plan que j'ai obtenu pour cette solution lors du forçage du traitement en série.

Figure 6 :Plan de série pour une nouvelle solution

Les trois fonctions de fenêtre utilisent la stratégie sans tri + mode batch série. Et les résultats sont assez impressionnants. Les temps d'exécution de cette solution variaient de 0,5 seconde contre 1 million de lignes et de 5,49 secondes contre 10 millions de lignes. Ce qui est également curieux à propos de cette solution, c'est que lors du test en tant qu'instruction SELECT normale (avec les résultats ignorés) par opposition à une instruction SELECT INTO, SQL Server a choisi le plan série par défaut. Avec les deux autres solutions, j'ai obtenu un plan parallèle par défaut avec SELECT et avec SELECT INTO.

Voir la section suivante pour les résultats complets des tests de performance.

Comparaison des performances

Voici le code que j'ai utilisé pour tester les trois solutions, en décommentant bien sûr l'indice MAXDOP 1 pour tester les plans série :

-- Tester la solution connue 1 SUPPRIMER LA TABLE SI EXISTE #Résultat ; WITH C AS( SELECT *, ROW_NUMBER() OVER(PARTITION BY grp ORDER BY ord) - ROW_NUMBER() OVER(PARTITION BY grp, val ORDER BY ord) AS island FROM dbo.T1)SELECT grp, ord, val, ROW_NUMBER( ) OVER(PARTITION BY grp, val, island ORDER BY ord) AS seqnoINTO #ResultFROM C/* OPTION(MAXDOP 1) */; -- décommenter pour testGO en série -- Tester la solution connue 2 DROP TABLE IF EXISTS #Result; WITH C1 AS( SELECT *, CASE WHEN val =LAG(val) OVER(PARTITION BY grp ORDER BY ord) THEN 0 ELSE 1 END AS isstart FROM dbo.T1),C2 AS( SELECT *, SUM(isstart) OVER(PARTITION BY grp ORDER BY ord ROWS UNBOUNDED PRECEDING) AS island FROM C1)SELECT grp, ord, val, ROW_NUMBER() OVER(PARTITION BY grp, island ORDER BY ord) AS seqnoINTO #ResultFROM C2/* OPTION(MAXDOP 1) */; GO -- Tester la nouvelle solution DROP TABLE IF EXISTS #Result; AVEC C1 AS( SELECT *, ROW_NUMBER() OVER(PARTITION BY grp ORDER BY ord) AS rn, LAG(val) OVER(PARTITION BY grp ORDER BY ord) AS prev FROM dbo.T1),C2 AS( SELECT *, MAX (CASE WHEN val =prev THEN NULL ELSE rn END) OVER(PARTITION BY grp ORDER BY ord ROWS UNBOUNDED PRECEDING) AS firstrn FROM C1)SELECT grp, ord, val, rn - firstrn + 1 AS seqnoINTO #ResultFROM C2/* OPTION( MAXDOP 1) */;

La figure 7 présente les durées d'exécution des plans parallèle et série pour toutes les solutions par rapport à différentes tailles d'entrée.

Figure 7 :Comparaison des performances

La nouvelle solution, utilisant le mode série, est clairement la gagnante. Il offre d'excellentes performances, une mise à l'échelle linéaire et un temps de réponse immédiat.

Conclusion

Les tâches des îles sont assez courantes dans la vraie vie. Beaucoup d'entre eux impliquent à la fois l'identification des îles et le regroupement des données par île. Le défi des îles d'Erland, qui était au centre de cet article, est un peu plus unique car il n'implique pas de grouper mais plutôt de séquencer les lignes de chaque île avec des numéros de ligne.

J'ai présenté deux solutions basées sur des techniques connues d'identification des îles. Le problème avec les deux est qu'ils entraînent des plans impliquant un tri explicite, ce qui affecte négativement les performances, l'évolutivité et le temps de réponse des solutions. J'ai également présenté une nouvelle technique qui a abouti à un plan sans tri du tout. Le plan série de cette solution, qui utilise une stratégie sans tri + mode batch série, offre d'excellentes performances, une mise à l'échelle linéaire et un temps de réponse immédiat. C'est dommage, du moins pour l'instant, nous ne pouvons pas avoir de stratégie sans tri + mode batch parallèle pour gérer les fonctions de la fenêtre.