Chaîne fractionnée T-SQL


139

J'ai une colonne SQL Server 2008 R2 contenant une chaîne que je dois diviser par une virgule. J'ai vu de nombreuses réponses sur StackOverflow mais aucune d'elles ne fonctionne dans R2. Je me suis assuré de disposer des autorisations de sélection sur tous les exemples de fonction fractionnée. Toute aide grandement appréciée.


7
C'est l'une des millions de réponses que j'aime stackoverflow.com/a/1846561/227755
nurettin

2
Que voulez-vous dire "aucun d'eux ne fonctionne"? Peux-tu être plus précis?
Aaron Bertrand le

Andy m'a orienté dans la bonne direction car j'exécutais la fonction de manière incorrecte. C'est pourquoi aucune des autres réponses de la pile n'a fonctionné. Ma faute.
Lee Grindon

2
duplication possible de la chaîne Split en SQL
Luv

Il y a une mdq.RegexSplitfonction dans le module complémentaire "Master Data Services", qui peut vous aider. Il vaut certainement la peine d'enquêter .
jpaugh

Réponses:


233

J'ai déjà utilisé ce SQL qui peut fonctionner pour vous: -

CREATE FUNCTION dbo.splitstring ( @stringToSplit VARCHAR(MAX) )
RETURNS
 @returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN

 DECLARE @name NVARCHAR(255)
 DECLARE @pos INT

 WHILE CHARINDEX(',', @stringToSplit) > 0
 BEGIN
  SELECT @pos  = CHARINDEX(',', @stringToSplit)  
  SELECT @name = SUBSTRING(@stringToSplit, 1, @pos-1)

  INSERT INTO @returnList 
  SELECT @name

  SELECT @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, LEN(@stringToSplit)-@pos)
 END

 INSERT INTO @returnList
 SELECT @stringToSplit

 RETURN
END

et pour l'utiliser: -

SELECT * FROM dbo.splitstring('91,12,65,78,56,789')

1
Gentil, c'est exactement ce que je cherchais merci beaucoup
Lee Grindon

2
Merci beaucoup Andy. J'ai apporté une petite amélioration à votre script pour permettre à la fonction de renvoyer un élément à un index spécifique dans la chaîne divisée. Cela n'est utile que dans les situations où la structure de la colonne est en cours d'analyse. gist.github.com/klimaye/8147193
CF_Maintainer

1
J'ai posté quelques améliorations (avec des cas de test de support) sur ma page github ici . Je le posterai comme réponse dans ce fil de discussion Stack Overflow quand j'aurai assez de représentants pour dépasser le poste "protection"
mpag

8
Bien que ce soit une excellente réponse, elle est dépassée ... Les approches procédurales (en particulier les boucles) sont quelque chose à éviter ... Cela vaut la peine de se pencher sur les nouvelles réponses ...
Shnugo

2
Je suis totalement d'accord avec @Shnugo. Les séparateurs en boucle fonctionnent mais horriblement lents. Quelque chose comme ça sqlservercentral.com/articles/Tally+Table/72993 est bien meilleur. Vous trouverez ici d'autres excellentes options basées sur des ensembles. sqlperformance.com/2012/07/t-sql-queries/split-strings
Sean Lange

61

Au lieu de CTE récursifs et de boucles while, quelqu'un a-t-il envisagé une approche plus basée sur les ensembles? Notez que cette fonction a été écrite pour la question, qui était basée sur SQL Server 2008 et la virgule comme délimiteur . Dans SQL Server 2016 et supérieur (et au niveau de compatibilité 130 et supérieur), STRING_SPLIT()est une meilleure option .

CREATE FUNCTION dbo.SplitString
(
  @List     nvarchar(max),
  @Delim    nvarchar(255)
)
RETURNS TABLE
AS
  RETURN ( SELECT [Value] FROM 
  ( 
    SELECT [Value] = LTRIM(RTRIM(SUBSTRING(@List, [Number],
      CHARINDEX(@Delim, @List + @Delim, [Number]) - [Number])))
    FROM (SELECT Number = ROW_NUMBER() OVER (ORDER BY name)
      FROM sys.all_columns) AS x WHERE Number <= LEN(@List)
      AND SUBSTRING(@Delim + @List, [Number], DATALENGTH(@Delim)/2) = @Delim
    ) AS y
  );
GO

Si vous souhaitez éviter que la longueur de la chaîne soit limitée à <= le nombre de lignes dans sys.all_columns(9,980 dans modelSQL Server 2017; beaucoup plus élevé dans vos propres bases de données utilisateur), vous pouvez utiliser d'autres approches pour dériver les nombres, telles que construire votre propre table de nombres . Vous pouvez également utiliser un CTE récursif dans les cas où vous ne pouvez pas utiliser de tables système ou créer les vôtres:

CREATE FUNCTION dbo.SplitString
(
  @List     nvarchar(max),
  @Delim    nvarchar(255)
)
RETURNS TABLE WITH SCHEMABINDING
AS
   RETURN ( WITH n(n) AS (SELECT 1 UNION ALL SELECT n+1 
       FROM n WHERE n <= LEN(@List))
       SELECT [Value] = SUBSTRING(@List, n, 
       CHARINDEX(@Delim, @List + @Delim, n) - n)
       FROM n WHERE n <= LEN(@List)
      AND SUBSTRING(@Delim + @List, n, DATALENGTH(@Delim)/2) = @Delim
   );
GO

Mais vous devrez ajouter OPTION (MAXRECURSION 0)(ou MAXRECURSION <longest possible string length if < 32768>) à la requête externe afin d'éviter les erreurs de récursivité pour les chaînes> 100 caractères. Si ce n'est pas non plus une bonne alternative, voyez cette réponse comme indiqué dans les commentaires.

(En outre, le délimiteur devra être NCHAR(<=1228). Je recherche toujours pourquoi.)

Plus d'informations sur les fonctions de fractionnement, pourquoi (et la preuve que) alors que les boucles et les CTE récursifs ne sont pas mis à l'échelle, et de meilleures alternatives, si les chaînes de fractionnement provenant de la couche d'application:


1
Il y a un petit bogue dans cette procédure pour le cas où il y aurait une valeur nulle à la fin de la chaîne - comme dans '1,2,, 4,' - car la valeur finale n'est pas analysée. Pour corriger ce bogue, l'expression "WHERE Number <= LEN (@List)" doit être remplacée par "WHERE Number <= LEN (@List) + 1".
SylvainL

@SylvainL Je suppose que cela dépend du comportement que vous voulez. D'après mon expérience, la plupart des gens veulent ignorer les virgules de fin car elles ne représentent pas vraiment un élément réel (de combien de copies d'une chaîne vide avez-vous besoin)? Quoi qu'il en soit, le vrai moyen de le faire - si vous suivez le deuxième lien - est de déconner avec le fractionnement de grosses chaînes laides de toute façon en T-SQL lent.
Aaron Bertrand

1
Comme vous l'avez dit, la plupart des gens veulent ignorer les virgules de fin, mais hélas, pas toutes. Je suppose qu'une solution plus complète consisterait à ajouter un paramètre pour spécifier ce qu'il faut faire dans ce cas mais mon commentaire est juste une petite note pour s'assurer que personne n'oublie cette possibilité, car elle peut être tout à fait réelle dans de nombreux cas.
SylvainL

J'ai un comportement étrange avec cette fonction. Si j'utilise directement une chaîne comme paramètre - cela fonctionne. Si j'ai un varchar, ce n'est pas le cas. Vous pouvez reproduire facilement: déclarer invarchar comme varchar set invarchar = 'ta; aa; qq' SELECT Valeur de [dbo]. [SplitString] (invarchar, ';') SELECT Valeur de [dbo]. [SplitString] ('ta; aa; qq ','; ')
Patrick Desjardins

J'aime cette approche, mais si le nombre d'objets renvoyés par sys.all_objectsest inférieur au nombre de caractères dans la chaîne d'entrée, elle tronquera la chaîne et les valeurs disparaîtront. Comme il sys.all_objectsest simplement utilisé comme un hack pour générer des lignes, il existe de meilleures façons de le faire, par exemple cette réponse .
knuckles

56

Enfin, l'attente est terminée dans SQL Server 2016, ils ont introduit la fonction de chaîne de fractionnement:STRING_SPLIT

select * From STRING_SPLIT ('a,b', ',') cs 

Toutes les autres méthodes pour diviser une chaîne comme XML, table Tally, boucle while, etc. ont été époustouflées par cela STRING_SPLIT fonction.

Voici un excellent article avec une comparaison des performances: Surprises et hypothèses de performances: STRING_SPLIT


5
répond évidemment à la question de savoir comment diviser la chaîne pour ceux qui ont des serveurs mis à jour, mais ceux d'entre nous encore bloqués sur 2008 / 2008R2 devront aller avec l'une des autres réponses ici.
mpag le

2
Vous devez examiner le niveau de compatibilité de votre base de données. S'il est inférieur à 130, vous ne pourrez pas utiliser la fonction STRING_SPLIT.
Luis Teijon

En fait, si la compatibilité n'est pas de 130 et que vous exécutez 2016 (ou Azure SQL), vous pouvez définir la compatibilité jusqu'à 130 en utilisant: ALTER DATABASE DatabaseName SET COMPATIBILITY_LEVEL = 130
Michieal

23

Le moyen le plus simple de le faire est d'utiliser le XMLformat.

1. Conversion de chaîne en lignes sans tableau

REQUETE

DECLARE @String varchar(100) = 'String1,String2,String3'
-- To change ',' to any other delimeter, just change ',' to your desired one
DECLARE @Delimiter CHAR = ','    

SELECT LTRIM(RTRIM(Split.a.value('.', 'VARCHAR(100)'))) 'Value' 
FROM  
(     
     SELECT CAST ('<M>' + REPLACE(@String, @Delimiter, '</M><M>') + '</M>' AS XML) AS Data            
) AS A 
CROSS APPLY Data.nodes ('/M') AS Split(a)

RÉSULTAT

 x---------x
 | Value   |
 x---------x
 | String1 |
 | String2 |
 | String3 |
 x---------x

2. Conversion en lignes d'une table avec un ID pour chaque ligne CSV

TABLEAU DES SOURCES

 x-----x--------------------------x
 | Id  |           Value          |
 x-----x--------------------------x
 |  1  |  String1,String2,String3 |
 |  2  |  String4,String5,String6 |     
 x-----x--------------------------x

REQUETE

-- To change ',' to any other delimeter, just change ',' before '</M><M>' to your desired one
DECLARE @Delimiter CHAR = ','

SELECT ID,LTRIM(RTRIM(Split.a.value('.', 'VARCHAR(100)'))) 'Value' 
FROM  
(     
     SELECT ID,CAST ('<M>' + REPLACE(VALUE, @Delimiter, '</M><M>') + '</M>' AS XML) AS Data            
     FROM TABLENAME
) AS A 
CROSS APPLY Data.nodes ('/M') AS Split(a)

RÉSULTAT

 x-----x----------x
 | Id  |  Value   |
 x-----x----------x
 |  1  |  String1 |
 |  1  |  String2 |  
 |  1  |  String3 |
 |  2  |  String4 |  
 |  2  |  String5 |
 |  2  |  String6 |     
 x-----x----------x

Cette approche se cassera si @Stringcontient des caractères interdits ... Je viens de poster une réponse pour surmonter ce problème.
Shnugo le

9

Je avais besoin d' un moyen rapide de se débarrasser du à +4partir d' un code postal .

UPDATE #Emails 
  SET ZIPCode = SUBSTRING(ZIPCode, 1, (CHARINDEX('-', ZIPCODE)-1)) 
  WHERE ZIPCode LIKE '%-%'

Pas de proc ... pas d'UDF ... juste une petite commande en ligne serrée qui fait ce qu'il faut. Pas de fantaisie, pas élégant.

Changez le délimiteur si nécessaire, etc., et cela fonctionnera pour tout.


4
Ce n'est pas le sujet de la question. L'OP a une valeur comme «234,542,23» et ils veulent le diviser en trois lignes ... 1ère ligne: 234, 2ème ligne: 542, 3ème ligne: 23. C'est une chose délicate à faire en SQL.
codeulike

7

si vous remplacez

WHILE CHARINDEX(',', @stringToSplit) > 0

avec

WHILE LEN(@stringToSplit) > 0

vous pouvez éliminer ce dernier insert après la boucle while!

CREATE FUNCTION dbo.splitstring ( @stringToSplit VARCHAR(MAX) )
RETURNS
 @returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN

 DECLARE @name NVARCHAR(255)
 DECLARE @pos INT

 WHILE LEN(@stringToSplit) > 0
 BEGIN
  SELECT @pos  = CHARINDEX(',', @stringToSplit)


if @pos = 0
        SELECT @pos = LEN(@stringToSplit)


  SELECT @name = SUBSTRING(@stringToSplit, 1, @pos-1)

  INSERT INTO @returnList 
  SELECT @name

  SELECT @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, LEN(@stringToSplit)-@pos)
 END

 RETURN
END

Cela entraînerait la troncature du dernier caractère du dernier élément. ie "AL, AL" deviendrait "AL" | "A" c'est-à-dire "ABC, ABC, ABC" deviendrait "ABC" | "ABC" | "AB"
Microsoft Developer

l'ajout +1de SELECT @pos = LEN(@stringToSplit)semble résoudre ce problème. Cependant, le SELECT @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, LEN(@stringToSplit)-@pos)retourne à Invalid length parameter passed to the LEFT or SUBSTRING functionmoins que vous ajoutiez également +1au troisième paramètre de SUBSTRING. ou vous pouvez remplacer cette affectation parSET @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, 4000) --MAX len of nvarchar is 4000
mpag

1
J'ai posté quelques améliorations (avec des cas de test de support) sur ma page github ici . Je le posterai comme réponse dans ce fil de discussion Stack Overflow quand j'aurai assez de représentants pour dépasser le poste "protection"
mpag

J'ai aussi noté le problème signalé par Terry ci-dessus. Mais la logique donnée par @AviG est tellement cool qu'elle n'échoue pas au milieu pour une longue liste de jetons. Essayez cet appel de test pour vérifier (cet appel devrait renvoyer 969 jetons) sélectionnez * de dbo.splitstring ('token1, token2 ,,,,,,,, token969') Ensuite, j'ai essayé le code donné par mpag pour vérifier les résultats pour le même appelez ci-dessus et a trouvé qu'il ne peut renvoyer que 365 jetons. Enfin, j'ai corrigé le code par AviG ci-dessus et publié la fonction sans bogue en tant que nouvelle réponse ci-dessous, car les commentaires ici ne permettent que du texte limité. Vérifiez la réponse sous mon nom pour l'essayer.
Gemunu R Wickremasinghe

3

Toutes les fonctions de fractionnement de chaînes qui utilisent une sorte de boucle (itérations) ont de mauvaises performances. Ils doivent être remplacés par une solution basée sur des ensembles.

Ce code s'exécute parfaitement.

CREATE FUNCTION dbo.SplitStrings
(
   @List       NVARCHAR(MAX),
   @Delimiter  NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
   RETURN 
   (  
      SELECT Item = y.i.value('(./text())[1]', 'nvarchar(4000)')
      FROM 
      ( 
        SELECT x = CONVERT(XML, '<i>' 
          + REPLACE(@List, @Delimiter, '</i><i>') 
          + '</i>').query('.')
      ) AS a CROSS APPLY x.nodes('i') AS y(i)
   );
GO

Cette approche se cassera si @Listcontient des caractères interdits ... Je viens de poster une réponse pour surmonter ce problème.
Shnugo le

Je vote pour votre réponse parce que la vôtre fonctionne avec un espace comme délimiteur et que le vote le plus élevé ne le fait pas
KMC

3

L'approche souvent utilisée avec les éléments XML se rompt en cas de caractères interdits. C'est une approche pour utiliser cette méthode avec n'importe quel type de caractère, même avec le point-virgule comme délimiteur.

L'astuce est la première à utiliser SELECT SomeString AS [*] FOR XML PATH('')pour échapper correctement à tous les caractères interdits. C'est la raison pour laquelle je remplace le délimiteur par une valeur magique pour éviter les problèmes avec ;comme délimiteur.

DECLARE @Dummy TABLE (ID INT, SomeTextToSplit NVARCHAR(MAX))
INSERT INTO @Dummy VALUES
 (1,N'A&B;C;D;E, F')
,(2,N'"C" & ''D'';<C>;D;E, F');

DECLARE @Delimiter NVARCHAR(10)=';'; --special effort needed (due to entities coding with "&code;")!

WITH Casted AS
(
    SELECT *
          ,CAST(N'<x>' + REPLACE((SELECT REPLACE(SomeTextToSplit,@Delimiter,N'§§Split$me$here§§') AS [*] FOR XML PATH('')),N'§§Split$me$here§§',N'</x><x>') + N'</x>' AS XML) AS SplitMe
    FROM @Dummy
)
SELECT Casted.ID
      ,x.value(N'.',N'nvarchar(max)') AS Part 
FROM Casted
CROSS APPLY SplitMe.nodes(N'/x') AS A(x)

Le résultat

ID  Part
1   A&B
1   C
1   D
1   E, F
2   "C" & 'D'
2   <C>
2   D
2   E, F

2

J'ai dû écrire quelque chose comme ça récemment. Voici la solution que j'ai trouvée. Il est généralisé pour toute chaîne de délimiteur et je pense que cela fonctionnerait légèrement mieux:

CREATE FUNCTION [dbo].[SplitString] 
    ( @string nvarchar(4000)
    , @delim nvarchar(100) )
RETURNS
    @result TABLE 
        ( [Value] nvarchar(4000) NOT NULL
        , [Index] int NOT NULL )
AS
BEGIN
    DECLARE @str nvarchar(4000)
          , @pos int 
          , @prv int = 1

    SELECT @pos = CHARINDEX(@delim, @string)
    WHILE @pos > 0
    BEGIN
        SELECT @str = SUBSTRING(@string, @prv, @pos - @prv)
        INSERT INTO @result SELECT @str, @prv

        SELECT @prv = @pos + LEN(@delim)
             , @pos = CHARINDEX(@delim, @string, @pos + 1)
    END

    INSERT INTO @result SELECT SUBSTRING(@string, @prv, 4000), @prv
    RETURN
END

1

Une solution utilisant un CTE, si quelqu'un en avait besoin (à part moi, qui l'a évidemment fait, c'est pourquoi je l'ai écrit).

declare @StringToSplit varchar(100) = 'Test1,Test2,Test3';
declare @SplitChar varchar(10) = ',';

with StringToSplit as (
  select 
      ltrim( rtrim( substring( @StringToSplit, 1, charindex( @SplitChar, @StringToSplit ) - 1 ) ) ) Head
    , substring( @StringToSplit, charindex( @SplitChar, @StringToSplit ) + 1, len( @StringToSplit ) ) Tail

  union all

  select
      ltrim( rtrim( substring( Tail, 1, charindex( @SplitChar, Tail ) - 1 ) ) ) Head
    , substring( Tail, charindex( @SplitChar, Tail ) + 1, len( Tail ) ) Tail
  from StringToSplit
  where charindex( @SplitChar, Tail ) > 0

  union all

  select
      ltrim( rtrim( Tail ) ) Head
    , '' Tail
  from StringToSplit
  where charindex( @SplitChar, Tail ) = 0
    and len( Tail ) > 0
)
select Head from StringToSplit

1

Ceci est plus étroitement adapté. Lorsque je fais cela, j'ai généralement une liste d'ID uniques séparés par des virgules (INT ou BIGINT), que je souhaite convertir en table à utiliser comme jointure interne vers une autre table qui a une clé primaire INT ou BIGINT. Je veux qu'une fonction table en ligne soit renvoyée afin d'avoir la jointure la plus efficace possible.

Un exemple d'utilisation serait:

 DECLARE @IDs VARCHAR(1000);
 SET @IDs = ',99,206,124,8967,1,7,3,45234,2,889,987979,';
 SELECT me.Value
 FROM dbo.MyEnum me
 INNER JOIN dbo.GetIntIdsTableFromDelimitedString(@IDs) ids ON me.PrimaryKey = ids.ID

J'ai volé l'idée de http://sqlrecords.blogspot.com/2012/11/converting-delimited-list-to-table.html , en la changeant pour qu'elle soit en ligne avec une valeur de table et convertie en INT.

create function dbo.GetIntIDTableFromDelimitedString
    (
    @IDs VARCHAR(1000)  --this parameter must start and end with a comma, eg ',123,456,'
                        --all items in list must be perfectly formatted or function will error
)
RETURNS TABLE AS
 RETURN

SELECT
    CAST(SUBSTRING(@IDs,Nums.number + 1,CHARINDEX(',',@IDs,(Nums.number+2)) - Nums.number - 1) AS INT) AS ID 
FROM   
     [master].[dbo].[spt_values] Nums
WHERE Nums.Type = 'P' 
AND    Nums.number BETWEEN 1 AND DATALENGTH(@IDs)
AND    SUBSTRING(@IDs,Nums.number,1) = ','
AND    CHARINDEX(',',@IDs,(Nums.number+1)) > Nums.number;

GO

1

Il existe une version correcte ici, mais j'ai pensé que ce serait bien d'ajouter un peu de tolérance aux pannes au cas où ils auraient une virgule à la fin ainsi que de la faire pour que vous puissiez l'utiliser non pas comme une fonction mais dans le cadre d'un plus grand morceau de code . Juste au cas où vous ne l'utilisez qu'une seule fois et n'avez pas besoin d'une fonction. C'est également pour les nombres entiers (ce dont j'avais besoin), vous devrez donc peut-être changer vos types de données.

DECLARE @StringToSeperate VARCHAR(10)
SET @StringToSeperate = '1,2,5'

--SELECT @StringToSeperate IDs INTO #Test

DROP TABLE #IDs
CREATE TABLE #IDs (ID int) 

DECLARE @CommaSeperatedValue NVARCHAR(255) = ''
DECLARE @Position INT = LEN(@StringToSeperate)

--Add Each Value
WHILE CHARINDEX(',', @StringToSeperate) > 0
BEGIN
    SELECT @Position  = CHARINDEX(',', @StringToSeperate)  
    SELECT @CommaSeperatedValue = SUBSTRING(@StringToSeperate, 1, @Position-1)

    INSERT INTO #IDs 
    SELECT @CommaSeperatedValue

    SELECT @StringToSeperate = SUBSTRING(@StringToSeperate, @Position+1, LEN(@StringToSeperate)-@Position)

END

--Add Last Value
IF (LEN(LTRIM(RTRIM(@StringToSeperate)))>0)
BEGIN
    INSERT INTO #IDs
    SELECT SUBSTRING(@StringToSeperate, 1, @Position)
END

SELECT * FROM #IDs

si vous deviez SET @StringToSeperate = @StringToSeperate+','immédiatement avant la WHILEboucle, je pense que vous pourriez être en mesure d'éliminer le bloc "ajouter la dernière valeur". Voir aussi mon sol'n sur github
mpag

Sur quelle réponse est-ce basé? Il y a beaucoup de réponses ici, et c'est un peu déroutant. Merci.
jpaugh

1

J'ai modifié un peu la fonction d'Andy Robinson. Maintenant, vous pouvez sélectionner uniquement la partie requise de la table de retour:

CREATE FUNCTION dbo.splitstring ( @stringToSplit VARCHAR(MAX) )

RETURNS

 @returnList TABLE ([numOrder] [tinyint] , [Name] [nvarchar] (500)) AS
BEGIN

 DECLARE @name NVARCHAR(255)

 DECLARE @pos INT

 DECLARE @orderNum INT

 SET @orderNum=0

 WHILE CHARINDEX('.', @stringToSplit) > 0

 BEGIN
    SELECT @orderNum=@orderNum+1;
  SELECT @pos  = CHARINDEX('.', @stringToSplit)  
  SELECT @name = SUBSTRING(@stringToSplit, 1, @pos-1)

  INSERT INTO @returnList 
  SELECT @orderNum,@name

  SELECT @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, LEN(@stringToSplit)-@pos)
 END
    SELECT @orderNum=@orderNum+1;
 INSERT INTO @returnList
 SELECT @orderNum, @stringToSplit

 RETURN
END


Usage:

SELECT Name FROM dbo.splitstring('ELIS.YD.CRP1.1.CBA.MDSP.T389.BT') WHERE numOrder=5


1

Si vous avez besoin d'une solution ad-hoc rapide pour les cas courants avec un code minimum, alors ce double ligne CTE récursif le fera:

DECLARE @s VARCHAR(200) = ',1,2,,3,,,4,,,,5,'

;WITH
a AS (SELECT i=-1, j=0 UNION ALL SELECT j, CHARINDEX(',', @s, j + 1) FROM a WHERE j > i),
b AS (SELECT SUBSTRING(@s, i+1, IIF(j>0, j, LEN(@s)+1)-i-1) s FROM a WHERE i >= 0)
SELECT * FROM b

Utilisez-le comme une instruction autonome ou ajoutez simplement les CTE ci-dessus à l'une de vos requêtes et vous pourrez rejoindre la table résultante b avec d'autres pour une utilisation dans d'autres expressions.

modifier (par Shnugo)

Si vous ajoutez un compteur, vous obtiendrez un index de position avec la liste:

DECLARE @s VARCHAR(200) = '1,2333,344,4'

;WITH
a AS (SELECT n=0, i=-1, j=0 UNION ALL SELECT n+1, j, CHARINDEX(',', @s, j+1) FROM a WHERE j > i),
b AS (SELECT n, SUBSTRING(@s, i+1, IIF(j>0, j, LEN(@s)+1)-i-1) s FROM a WHERE i >= 0)
SELECT * FROM b;

Le résultat:

n   s
1   1
2   2333
3   344
4   4

J'aime cette approche. J'espère que cela ne vous dérange pas, que j'ai ajouté une amélioration directement dans votre réponse.
N'hésitez

1

Je prends la route xml en enveloppant les valeurs dans des éléments (M mais tout fonctionne):

declare @v nvarchar(max) = '100,201,abcde'

select 
    a.value('.', 'varchar(max)')
from
    (select cast('<M>' + REPLACE(@v, ',', '</M><M>') + '</M>' AS XML) as col) as A
    CROSS APPLY A.col.nodes ('/M') AS Split(a)

0

voici une version qui peut se dédoubler sur un motif en utilisant patindex, une simple adaptation du post ci-dessus. J'ai eu un cas où je devais diviser une chaîne contenant plusieurs caractères de séparation.


alter FUNCTION dbo.splitstring ( @stringToSplit VARCHAR(1000), @splitPattern varchar(10) )
RETURNS
 @returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN

 DECLARE @name NVARCHAR(255)
 DECLARE @pos INT

 WHILE PATINDEX(@splitPattern, @stringToSplit) > 0
 BEGIN
  SELECT @pos  = PATINDEX(@splitPattern, @stringToSplit)  
  SELECT @name = SUBSTRING(@stringToSplit, 1, @pos-1)

  INSERT INTO @returnList 
  SELECT @name

  SELECT @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, LEN(@stringToSplit)-@pos)
 END

 INSERT INTO @returnList
 SELECT @stringToSplit

 RETURN
END
select * from dbo.splitstring('stringa/stringb/x,y,z','%[/,]%');

le résultat ressemble à ceci

stringa stringb x y z


0

Personnellement j'utilise cette fonction:

ALTER FUNCTION [dbo].[CUST_SplitString]
(
    @String NVARCHAR(4000),
    @Delimiter NCHAR(1)
)
RETURNS TABLE 
AS
RETURN 
(
    WITH Split(stpos,endpos) 
    AS(
        SELECT 0 AS stpos, CHARINDEX(@Delimiter,@String) AS endpos
        UNION ALL
        SELECT endpos+1, CHARINDEX(@Delimiter,@String,endpos+1) 
        FROM Split
        WHERE endpos > 0
    )
    SELECT 'Id' = ROW_NUMBER() OVER (ORDER BY (SELECT 1)),
        'Data' = SUBSTRING(@String,stpos,COALESCE(NULLIF(endpos,0),LEN(@String)+1)-stpos)
    FROM Split
)

0

J'ai développé un double séparateur (prend deux caractères séparés) comme demandé ici . Cela pourrait avoir une certaine valeur dans ce thread car il est le plus référencé pour les requêtes relatives au fractionnement de chaînes.

CREATE FUNCTION uft_DoubleSplitter 
(   
    -- Add the parameters for the function here
    @String VARCHAR(4000), 
    @Splitter1 CHAR,
    @Splitter2 CHAR
)
RETURNS @Result TABLE (Id INT,MId INT,SValue VARCHAR(4000))
AS
BEGIN
DECLARE @FResult TABLE(Id INT IDENTITY(1, 1),
                   SValue VARCHAR(4000))
DECLARE @SResult TABLE(Id INT IDENTITY(1, 1),
                   MId INT,
                   SValue VARCHAR(4000))
SET @String = @String+@Splitter1

WHILE CHARINDEX(@Splitter1, @String) > 0
    BEGIN
       DECLARE @WorkingString VARCHAR(4000) = NULL

       SET @WorkingString = SUBSTRING(@String, 1, CHARINDEX(@Splitter1, @String) - 1)
       --Print @workingString

       INSERT INTO @FResult
       SELECT CASE
            WHEN @WorkingString = '' THEN NULL
            ELSE @WorkingString
            END

       SET @String = SUBSTRING(@String, LEN(@WorkingString) + 2, LEN(@String))

    END
IF ISNULL(@Splitter2, '') != ''
    BEGIN
       DECLARE @OStartLoop INT
       DECLARE @OEndLoop INT

       SELECT @OStartLoop = MIN(Id),
            @OEndLoop = MAX(Id)
       FROM @FResult

       WHILE @OStartLoop <= @OEndLoop
          BEGIN
             DECLARE @iString VARCHAR(4000)
             DECLARE @iMId INT

             SELECT @iString = SValue+@Splitter2,
                   @iMId = Id
             FROM @FResult
             WHERE Id = @OStartLoop

             WHILE CHARINDEX(@Splitter2, @iString) > 0
                BEGIN
                    DECLARE @iWorkingString VARCHAR(4000) = NULL

                    SET @IWorkingString = SUBSTRING(@iString, 1, CHARINDEX(@Splitter2, @iString) - 1)

                    INSERT INTO @SResult
                    SELECT @iMId,
                         CASE
                         WHEN @iWorkingString = '' THEN NULL
                         ELSE @iWorkingString
                         END

                    SET @iString = SUBSTRING(@iString, LEN(@iWorkingString) + 2, LEN(@iString))

                END

             SET @OStartLoop = @OStartLoop + 1
          END
       INSERT INTO @Result
       SELECT MId AS PrimarySplitID,
            ROW_NUMBER() OVER (PARTITION BY MId ORDER BY Mid, Id) AS SecondarySplitID ,
            SValue
       FROM @SResult
    END
ELSE
    BEGIN
       INSERT INTO @Result
       SELECT Id AS PrimarySplitID,
            NULL AS SecondarySplitID,
            SValue
       FROM @FResult
    END
RETURN

Usage:

--FirstSplit
SELECT * FROM uft_DoubleSplitter('ValueA=ValueB=ValueC=ValueD==ValueE&ValueA=ValueB=ValueC===ValueE&ValueA=ValueB==ValueD===','&',NULL)

--Second Split
SELECT * FROM uft_DoubleSplitter('ValueA=ValueB=ValueC=ValueD==ValueE&ValueA=ValueB=ValueC===ValueE&ValueA=ValueB==ValueD===','&','=')

Utilisation possible (obtenir la deuxième valeur de chaque fractionnement):

SELECT fn.SValue
FROM uft_DoubleSplitter('ValueA=ValueB=ValueC=ValueD==ValueE&ValueA=ValueB=ValueC===ValueE&ValueA=ValueB==ValueD===', '&', '=')AS fn
WHERE fn.mid = 2

0

Voici un exemple que vous pouvez utiliser comme fonction ou également mettre la même logique en procédure. --SELECT * à partir de [dbo] .fn_SplitString;

CREATE FUNCTION [dbo].[fn_SplitString]
(@CSV VARCHAR(MAX), @Delimeter VARCHAR(100) = ',')
       RETURNS @retTable TABLE 
(

    [value] VARCHAR(MAX) NULL
)AS

BEGIN

DECLARE
       @vCSV VARCHAR (MAX) = @CSV,
       @vDelimeter VARCHAR (100) = @Delimeter;

IF @vDelimeter = ';'
BEGIN
    SET @vCSV = REPLACE(@vCSV, ';', '~!~#~');
    SET @vDelimeter = REPLACE(@vDelimeter, ';', '~!~#~');
END;

SET @vCSV = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(@vCSV, '&', '&amp;'), '<', '&lt;'), '>', '&gt;'), '''', '&apos;'), '"', '&quot;');

DECLARE @xml XML;

SET @xml = '<i>' + REPLACE(@vCSV, @vDelimeter, '</i><i>') + '</i>';

INSERT INTO @retTable
SELECT
       x.i.value('.', 'varchar(max)') AS COLUMNNAME
  FROM @xml.nodes('//i')AS x(i);

 RETURN;
END;

Cette approche se cassera si @vCSVcontient des caractères interdits ... Je viens de poster une réponse pour surmonter ce problème.
Shnugo

0

Une solution récursive basée sur le cte

declare @T table (iden int identity, col1 varchar(100));
insert into @T(col1) values
       ('ROOT/South America/Lima/Test/Test2')
     , ('ROOT/South America/Peru/Test/Test2')
     , ('ROOT//South America/Venuzuala ')
     , ('RtT/South America / ') 
     , ('ROOT/South Americas// '); 
declare @split char(1) = '/';
select @split as split;
with cte as 
(  select t.iden, case when SUBSTRING(REVERSE(rtrim(t.col1)), 1, 1) = @split then LTRIM(RTRIM(t.col1)) else LTRIM(RTRIM(t.col1)) + @split end  as col1, 0 as pos                             , 1 as cnt
   from @T t
   union all 
   select t.iden, t.col1                                                                                                                              , charindex(@split, t.col1, t.pos + 1), cnt + 1 
   from cte t 
   where charindex(@split, t.col1, t.pos + 1) > 0 
)
select t1.*, t2.pos, t2.cnt
     , ltrim(rtrim(SUBSTRING(t1.col1, t1.pos+1, t2.pos-t1.pos-1))) as bingo
from cte t1 
join cte t2 
  on t2.iden = t1.iden 
 and t2.cnt  = t1.cnt+1
 and t2.pos > t1.pos 
order by t1.iden, t1.cnt;

0

Ceci est basé sur la réponse d'Andy Robertson, j'avais besoin d'un délimiteur autre que la virgule.

CREATE FUNCTION dbo.splitstring ( @stringToSplit nvarchar(MAX), @delim nvarchar(max))
RETURNS
 @returnList TABLE ([value] [nvarchar] (MAX))
AS
BEGIN

 DECLARE @value NVARCHAR(max)
 DECLARE @pos INT

 WHILE CHARINDEX(@delim, @stringToSplit) > 0
 BEGIN
  SELECT @pos  = CHARINDEX(@delim, @stringToSplit)  
  SELECT @value = SUBSTRING(@stringToSplit, 1, @pos - 1)

  INSERT INTO @returnList 
  SELECT @value

  SELECT @stringToSplit = SUBSTRING(@stringToSplit, @pos + LEN(@delim), LEN(@stringToSplit) - @pos)
 END

 INSERT INTO @returnList
 SELECT @stringToSplit

 RETURN
END
GO

Et pour l'utiliser:

SELECT * FROM dbo.splitstring('test1 test2 test3', ' ');

(Testé sur SQL Server 2008 R2)

EDIT: code de test correct


0

/ *

Réponse à la chaîne fractionnée T-SQL
Basé sur les réponses d' Andy Robinson et d' AviG
Fonctionnalité améliorée ref: fonction LEN sans espaces de fin dans SQL Server
Ce `` fichier '' doit être valide à la fois comme fichier de démarque et comme fichier SQL


*/

    CREATE FUNCTION dbo.splitstring ( --CREATE OR ALTER
        @stringToSplit NVARCHAR(MAX)
    ) RETURNS @returnList TABLE ([Item] NVARCHAR (MAX))
    AS BEGIN
        DECLARE @name NVARCHAR(MAX)
        DECLARE @pos BIGINT
        SET @stringToSplit = @stringToSplit + ','             -- this should allow entries that end with a `,` to have a blank value in that "column"
        WHILE ((LEN(@stringToSplit+'_') > 1)) BEGIN           -- `+'_'` gets around LEN trimming terminal spaces. See URL referenced above
            SET @pos = COALESCE(NULLIF(CHARINDEX(',', @stringToSplit),0),LEN(@stringToSplit+'_')) -- COALESCE grabs first non-null value
            SET @name = SUBSTRING(@stringToSplit, 1, @pos-1)  --MAX size of string of type nvarchar is 4000 
            SET @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, 4000) -- With SUBSTRING fn (MS web): "If start is greater than the number of characters in the value expression, a zero-length expression is returned."
            INSERT INTO @returnList SELECT @name --additional debugging parameters below can be added
            -- + ' pos:' + CAST(@pos as nvarchar) + ' remain:''' + @stringToSplit + '''(' + CAST(LEN(@stringToSplit+'_')-1 as nvarchar) + ')'
        END
        RETURN
    END
    GO

/*

Cas de test: voir l'URL référencée en tant que "fonctionnalité améliorée" ci-dessus

SELECT *,LEN(Item+'_')-1 'L' from splitstring('a,,b')

Item | L
---  | ---
a    | 1
     | 0
b    | 1

SELECT *,LEN(Item+'_')-1 'L' from splitstring('a,,')

Item | L   
---  | ---
a    | 1
     | 0
     | 0

SELECT *,LEN(Item+'_')-1 'L' from splitstring('a,, ')

Item | L   
---  | ---
a    | 1
     | 0
     | 1

SELECT *,LEN(Item+'_')-1 'L' from splitstring('a,, c ')

Item | L   
---  | ---
a    | 1
     | 0
 c   | 3

* /


rolled back to honour "Ce 'fichier' doit être valide à la fois comme un fichier markdown et un fichier SQL"
mpag

-1
ALTER FUNCTION [dbo].func_split_string
(
    @input as varchar(max),
    @delimiter as varchar(10) = ";"

)
RETURNS @result TABLE
(
    id smallint identity(1,1),
    csv_value varchar(max) not null
)
AS
BEGIN
    DECLARE @pos AS INT;
    DECLARE @string AS VARCHAR(MAX) = '';

    WHILE LEN(@input) > 0
    BEGIN           
        SELECT @pos = CHARINDEX(@delimiter,@input);

        IF(@pos<=0)
            select @pos = len(@input)

        IF(@pos <> LEN(@input))
            SELECT @string = SUBSTRING(@input, 1, @pos-1);
        ELSE
            SELECT @string = SUBSTRING(@input, 1, @pos);

        INSERT INTO @result SELECT @string

        SELECT @input = SUBSTRING(@input, @pos+len(@delimiter), LEN(@input)-@pos)       
    END
    RETURN  
END

-1

Vous pouvez utiliser cette fonction:

        CREATE FUNCTION SplitString
        (    
           @Input NVARCHAR(MAX),
           @Character CHAR(1)
          )
            RETURNS @Output TABLE (
            Item NVARCHAR(1000)
          )
        AS
        BEGIN

      DECLARE @StartIndex INT, @EndIndex INT
      SET @StartIndex = 1
      IF SUBSTRING(@Input, LEN(@Input) - 1, LEN(@Input)) <> @Character
      BEGIN
            SET @Input = @Input + @Character
      END

      WHILE CHARINDEX(@Character, @Input) > 0
      BEGIN
            SET @EndIndex = CHARINDEX(@Character, @Input)

            INSERT INTO @Output(Item)
            SELECT SUBSTRING(@Input, @StartIndex, @EndIndex - 1)

            SET @Input = SUBSTRING(@Input, @EndIndex + 1, LEN(@Input))
      END

      RETURN
END
GO

-1

Avec tout le respect que je dois à @AviG, il s'agit de la version sans bogue de la fonction conçue par lui pour renvoyer tous les jetons dans leur intégralité.

IF EXISTS (SELECT * FROM sys.objects WHERE type = 'TF' AND name = 'TF_SplitString')
DROP FUNCTION [dbo].[TF_SplitString]
GO

-- =============================================
-- Author:  AviG
-- Amendments:  Parameterize the delimeter and included the missing chars in last token - Gemunu Wickremasinghe
-- Description: Tabel valued function that Breaks the delimeted string by given delimeter and returns a tabel having split results
-- Usage
-- select * from   [dbo].[TF_SplitString]('token1,token2,,,,,,,,token969',',')
-- 969 items should be returned
-- select * from   [dbo].[TF_SplitString]('4672978261,4672978255',',')
-- 2 items should be returned
-- =============================================
CREATE FUNCTION dbo.TF_SplitString 
( @stringToSplit VARCHAR(MAX) ,
  @delimeter char = ','
)
RETURNS
 @returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN

    DECLARE @name NVARCHAR(255)
    DECLARE @pos INT

    WHILE LEN(@stringToSplit) > 0
    BEGIN
        SELECT @pos  = CHARINDEX(@delimeter, @stringToSplit)


        if @pos = 0
        BEGIN
            SELECT @pos = LEN(@stringToSplit)
            SELECT @name = SUBSTRING(@stringToSplit, 1, @pos)  
        END
        else 
        BEGIN
            SELECT @name = SUBSTRING(@stringToSplit, 1, @pos-1)
        END

        INSERT INTO @returnList 
        SELECT @name

        SELECT @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, LEN(@stringToSplit)-@pos)
    END

 RETURN
END

-3

Le moyen le plus simple:

  1. Installez SQL Server 2016
  2. Utilisez STRING_SPLIT https://msdn.microsoft.com/en-us/library/mt684588.aspx

Cela fonctionne même en édition express :).


N'oubliez pas de définir «Niveau de compatibilité» sur SQL Server 2016 (130) - dans le studio de gestion, faites un clic droit sur la base de données, les propriétés / options / le niveau de compatibilité.
Tomino

1
Le message original disait pour SQL 2008 R2. L'installation de SQL 2016 n'est peut-être pas une option
Shawn Gavett
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.