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

Surprises et hypothèses de performance :STRING_SPLIT()

Il y a plus de trois ans maintenant, j'ai publié une série en trois parties sur le fractionnement des cordes :

  • Séparez les cordes dans le bon sens, ou dans le meilleur sens suivant
  • Fractionner des chaînes :un suivi
  • Fractionner les chaînes :désormais avec moins de T-SQL

Puis en janvier, j'ai abordé un problème un peu plus élaboré :

  • Comparaison des méthodes de fractionnement/concaténation de chaînes

Tout au long, ma conclusion a été :ARRÊTEZ DE FAIRE CECI DANS T-SQL . Utilisez CLR ou, mieux encore, transmettez des paramètres structurés tels que DataTables de votre application à des paramètres table (TVP) dans vos procédures, en évitant toute construction et déconstruction de chaînes - ce qui est vraiment la partie de la solution qui cause des problèmes de performances.

Et puis SQL Server 2016 est arrivé…

Lors de la sortie de RC0, une nouvelle fonction a été documentée sans grand bruit :STRING_SPLIT . Un exemple rapide :

SELECT * FROM STRING_SPLIT('a,b,cd', ','); /* résultat :valeur -------- a b cd*/

Il a attiré l'attention de quelques collègues, dont Dave Ballantyne, qui a écrit sur les principales fonctionnalités - mais a eu la gentillesse de m'offrir le premier droit de refus sur une comparaison de performances.

Il s'agit principalement d'un exercice académique, car avec un ensemble important de limitations dans la première itération de la fonctionnalité, cela ne sera probablement pas réalisable pour un grand nombre de cas d'utilisation. Voici la liste des observations que Dave et moi avons faites, dont certaines peuvent être déterminantes dans certains scénarios :

  • la fonction nécessite que la base de données soit au niveau de compatibilité 130 ;
  • il n'accepte que les délimiteurs à un seul caractère ;
  • il n'y a aucun moyen d'ajouter des colonnes de sortie (comme une colonne indiquant la position ordinale dans la chaîne) ;
    • lié, il n'y a aucun moyen de contrôler le tri - les seules options sont arbitraires et alphabétiques ORDER BY value;
  • jusqu'à présent, il estime toujours 50 lignes de sortie ;
  • lorsque vous l'utilisez pour DML, dans de nombreux cas, vous obtiendrez une bobine de table (pour la protection de l'Halloween) ;
  • NULL l'entrée mène à un résultat vide ;
  • il n'y a aucun moyen de repousser les prédicats, comme éliminer les doublons ou les chaînes vides en raison de délimiteurs consécutifs ;
  • il n'y a aucun moyen d'effectuer des opérations sur les valeurs de sortie avant le fait (par exemple, de nombreuses fonctions de fractionnement exécutent LTRIM/RTRIM ou conversions explicites pour vous – STRING_SPLIT recrache tout ce qui est laid, comme les espaces de début).

Donc, avec ces limitations à l'air libre, nous pouvons passer à des tests de performances. Étant donné les antécédents de Microsoft avec des fonctions intégrées qui tirent parti du CLR sous les couvertures (touss FORMAT() toux ), j'étais sceptique quant à savoir si cette nouvelle fonction pouvait se rapprocher des méthodes les plus rapides que j'avais testées à ce jour.

Utilisons des séparateurs de chaînes pour séparer les chaînes de nombres séparées par des virgules, de cette façon notre nouvel ami JSON peut venir jouer aussi. Et on va dire qu'aucune liste ne peut excéder 8 000 caractères, donc pas de MAX les types sont obligatoires, et comme ce sont des nombres, nous n'avons pas à nous occuper de quelque chose d'exotique comme Unicode.

Commençons par créer nos fonctions, dont plusieurs que j'ai adaptées du premier article ci-dessus. J'ai laissé de côté un couple qui, à mon avis, ne serait pas en compétition; Je vais le laisser comme exercice au lecteur pour les tester.

    Tableau des nombres

    Celui-ci a encore besoin d'être configuré, mais il peut s'agir d'un tableau assez petit en raison des limitations artificielles que nous imposons :

    SET NOCOUNT ON ; DÉCLARER @UpperLimit INT =8000 ;;WITH n AS( SELECT x =ROW_NUMBER() OVER (ORDER BY s1.[object_id]) FROM sys.all_objects AS s1 CROSS JOIN sys.all_objects AS s2)SELECT Number =x INTO dbo.Numbers FROM n WHERE x BETWEEN 1 AND @UpperLimit;GOCREATE UNIQUE CLUSTERED INDEX n ON dbo.Numbers(Number);

    Puis la fonction :

    CREATE FUNCTION dbo.SplitStrings_Numbers( @List varchar(8000), @Delimiter char(1))RETURNS TABLE WITH SCHEMABINDINGAS RETURN ( SELECT [Value] =SUBSTRING(@List, [Number], CHARINDEX(@Delimiter, @List + @Delimiter, [Number]) - [Number]) FROM dbo.Numbers WHERE Number <=LEN(@List) AND SUBSTRING(@Delimiter + @List, [Number], 1) =@Delimiter );

    JSON

    Sur la base d'une approche révélée pour la première fois par l'équipe du moteur de stockage, j'ai créé un wrapper similaire autour de OPENJSON , notez simplement que le délimiteur doit être une virgule dans ce cas, ou vous devez effectuer une substitution de chaîne lourde avant de transmettre la valeur à la fonction native :

    CREATE FUNCTION dbo.SplitStrings_JSON( @List varchar(8000), @Delimiter char(1) -- ignoré mais facilite les tests automatisés)RETURNS TABLE WITH SCHEMABINDINGAS RETURN (SELECT value FROM OPENJSON( CHAR(91) + @List + CHAR(93) ));

    Les CHAR(91)/CHAR(93) remplacent simplement [ et ] respectivement en raison de problèmes de formatage.

    XML

    CREATE FUNCTION dbo.SplitStrings_XML( @List varchar(8000), @Delimiter char(1))RETURNS TABLE WITH SCHEMABINDINGAS RETURN (SELECT [value] =y.i.value('(./text())[1]', 'varchar(8000)') FROM (SELECT x =CONVERT(XML, '' + REPLACE(@List, @Delimiter, '') + '').query ('.') ) AS a CROSS APPLY x.nodes('i') AS y(i));

    CLR

    J'ai encore une fois emprunté le fidèle code de fractionnement d'Adam Machanic d'il y a presque sept ans, même s'il prend en charge Unicode, MAX types et délimiteurs multi-caractères (et en fait, parce que je ne veux pas du tout jouer avec le code de la fonction, cela limite nos chaînes d'entrée à 4 000 caractères au lieu de 8 000) :

    CREATE FUNCTION dbo.SplitStrings_CLR( @List nvarchar(MAX), @Delimiter nvarchar(255))RETURNS TABLE ( value nvarchar(4000) )EXTERNAL NAME CLRUtilities.UserDefinedFunctions.SplitString_Multi;

    STRING_SPLIT

    Par souci de cohérence, j'ai mis un wrapper autour de STRING_SPLIT :

    CREATE FUNCTION dbo.SplitStrings_Native( @List varchar(8000), @Delimiter char(1))RETURNS TABLE WITH SCHEMABINDINGAS RETURN (SELECT value FROM STRING_SPLIT(@List, @Delimiter));

Données source et vérification de l'intégrité

J'ai créé cette table pour servir de source de chaînes d'entrée aux fonctions :

CREATE TABLE dbo.SourceTable( RowNum int IDENTITY(1,1) PRIMARY KEY, StringValue varchar(8000));;AVEC x AS ( SELECT TOP (60000) x =STUFF((SELECT TOP (ABS(o.[object_id] % 20)) ',' + CONVERT(varchar(12), c.[object_id]) FROM sys.all_columns AS c WHERE c.[object_id]  

À titre de référence, validons que 50 000 lignes sont entrées dans le tableau, et vérifions la longueur moyenne de la chaîne et le nombre moyen d'éléments par chaîne :

SELECT [Valeurs] =COUNT(*), AvgStringLength =AVG(1.0*LEN(StringValue)), AvgElementCount =AVG(1.0*LEN(StringValue)-LEN(REPLACE(StringValue, ',','')) ) FROM dbo. SourceTable ; /* résultat :Valeurs AvgStringLength AbgElementCount ------ --------------- --------------- 50000 108.476380 8.911840*/ 

Et enfin, assurons-nous que chaque fonction renvoie les bonnes données pour un RowNum donné , nous allons donc en choisir une au hasard et comparer les valeurs obtenues par chaque méthode. Vos résultats varieront bien sûr.

SELECT f.value FROM dbo.SourceTable AS s CROSS APPLY dbo.SplitStrings_/* method */(s.StringValue, ',') AS f WHERE s.RowNum =37219 ORDER BY f.value;

Effectivement, toutes les fonctions fonctionnent comme prévu (le tri n'est pas numérique ; rappelez-vous, les fonctions génèrent des chaînes) :

Exemple de sortie de chacune des fonctions

Tests de performances

SELECT SYSDATETIME();GODECLARE @x VARCHAR(8000);SELECT @x =f.value FROM dbo.SourceTable AS s CROSS APPLY dbo.SplitStrings_/* méthode */(s.StringValue,',') AS f;GO 100SELECT SYSDATETIME();

J'ai exécuté le code ci-dessus 10 fois pour chaque méthode et fait la moyenne des délais pour chacune. Et c'est là que la surprise est arrivée pour moi. Compte tenu des limitations du STRING_SPLIT natif fonction, mon hypothèse était qu'il a été mis en place rapidement, et que la performance donnerait du crédit à cela. Garçon était le résultat différent de ce à quoi je m'attendais :

Durée moyenne de STRING_SPLIT par rapport aux autres méthodes

Mise à jour 2016-03-20

Sur la base de la question ci-dessous de Lars, j'ai relancé les tests avec quelques modifications :

  • J'ai surveillé mon instance avec SQL Sentry Performance Advisor pour capturer le profil du processeur pendant le test ;
  • J'ai capturé des statistiques d'attente au niveau de la session entre chaque lot ;
  • J'ai inséré un délai entre les lots afin que l'activité soit visuellement distincte sur le tableau de bord Performance Advisor.

J'ai créé un nouveau tableau pour capturer les informations sur les statistiques d'attente :

CREATE TABLE dbo.Timings( dt datetime, test varchar(64), point varchar(64), session_id smallint, wait_type nvarchar(60), wait_time_ms bigint,);

Ensuite, le code de chaque test a été remplacé par :

ATTENTE DELAI '00:00:30' ; DECLARE @d DATETIME =SYSDATETIME(); INSERT dbo.Timings(dt, test, point, wait_type, wait_time_ms)SELECT @d, test =/* 'method' */, point ='Start', wait_type, wait_time_msFROM sys.dm_exec_session_wait_stats WHERE session_id =@@SPID;GO DECLARE @x VARCHAR(8000);SELECT @x =f.value FROM dbo.SourceTable AS s CROSS APPLY dbo.SplitStrings_/* méthode */(s.StringValue, ',') AS fGO 100 DECLARE @d DATETIME =SYSDATETIME(); INSERT dbo.Timings(dt, test, point, wait_type, wait_time_ms)SELECT @d, /* 'method' */, 'End', wait_type, wait_time_msFROM sys.dm_exec_session_wait_stats WHERE session_id =@@SPID;

J'ai exécuté le test, puis j'ai exécuté les requêtes suivantes :

-- valider que les timings étaient dans le même stade que les tests précédentsSELECT test, DATEDIFF(SECOND, MIN(dt), MAX(dt)) FROM dbo.Timings WITH (NOLOCK)GROUP BY test ORDER BY 2 DESC; -- déterminer la fenêtre à appliquer au tableau de bord Performance AdvisorSELECT MIN(dt), MAX(dt) FROM dbo.Timings ; -- obtenir les statistiques d'attente enregistrées pour chaque test sessionSELECT, wait_type, delta FROM( SELECT f.test, rn =RANK() OVER (PARTITION BY f.point ORDER BY f.dt), f.wait_type, delta =f.wait_time_ms - COALESCE(s.wait_time_ms, 0) FROM dbo.Timings AS f LEFT OUTER JOIN dbo.Timings AS s ON s.test =f.test AND s.wait_type =f.wait_type AND s.point ='Start' WHERE f.point ='Fin') AS x WHERE delta> 0ORDER BY rn, delta DESC;

Dès la première requête, les timings sont restés cohérents avec les tests précédents (je les tracerais à nouveau mais cela ne révélerait rien de nouveau).

A partir de la deuxième requête, j'ai pu mettre en évidence cette plage sur le tableau de bord Performance Advisor, et à partir de là, il était facile d'identifier chaque lot :

Batchs capturés sur le graphique CPU sur le tableau de bord Performance Advisor

En clair, toutes les méthodes *sauf* STRING_SPLIT attaché un seul cœur pour la durée du test (il s'agit d'une machine quadricœur et le processeur était régulièrement à 25%). Il est probable que Lars insinuait ci-dessous que STRING_SPLIT est plus rapide au prix de marteler le CPU, mais il ne semble pas que ce soit le cas.

Enfin, à partir de la troisième requête, j'ai pu voir les statistiques d'attente suivantes s'accumuler après chaque lot :

Attentes par session, en millisecondes

Les attentes capturées par le DMV n'expliquent pas entièrement la durée des requêtes, mais elles servent à montrer où supplémentaire des attentes sont encourues.

Conclusion

Bien que le CLR personnalisé présente toujours un énorme avantage par rapport aux approches T-SQL traditionnelles, et que l'utilisation de JSON pour cette fonctionnalité semble n'être rien de plus qu'une nouveauté, STRING_SPLIT était le grand gagnant – d'un mile. Donc, si vous avez juste besoin de diviser une chaîne et que vous pouvez gérer toutes ses limitations, il semble que ce soit une option beaucoup plus viable que ce à quoi je m'attendais. Espérons que dans les futures versions, nous verrons des fonctionnalités supplémentaires, telles qu'une colonne de sortie indiquant la position ordinale de chaque élément, la possibilité de filtrer les doublons et les chaînes vides, et des délimiteurs multi-caractères.

J'adresse plusieurs commentaires ci-dessous dans deux articles de suivi :

  • STRING_SPLIT() dans SQL Server 2016 : Suivi #1
  • STRING_SPLIT() dans SQL Server 2016 : Suivi #2