Transformation d'une chaîne séparée par des virgules en lignes individuelles


234

J'ai une table SQL comme celle-ci:

| SomeID         | OtherID     | Data
+----------------+-------------+-------------------
| abcdef-.....   | cdef123-... | 18,20,22
| abcdef-.....   | 4554a24-... | 17,19
| 987654-.....   | 12324a2-... | 13,19,20

y a-t-il une requête où je peux effectuer une requête comme SELECT OtherID, SplitData WHERE SomeID = 'abcdef-.......'celle-ci renvoie des lignes individuelles, comme ceci:

| OtherID     | SplitData
+-------------+-------------------
| cdef123-... | 18
| cdef123-... | 20
| cdef123-... | 22
| 4554a24-... | 17
| 4554a24-... | 19

Fondamentalement, diviser mes données à la virgule en lignes individuelles?

Je suis conscient que le stockage d'une comma-separatedchaîne dans une base de données relationnelle semble stupide, mais le cas d'utilisation normal dans l'application grand public le rend vraiment utile.

Je ne veux pas faire la division dans l'application car j'ai besoin de pagination, donc je voulais explorer les options avant de refactoriser l'application entière.

C'est SQL Server 2008(non-R2).


Réponses:


265

Vous pouvez utiliser les merveilleuses fonctions récursives de SQL Server:


Exemple de tableau:

CREATE TABLE Testdata
(
    SomeID INT,
    OtherID INT,
    String VARCHAR(MAX)
)

INSERT Testdata SELECT 1,  9, '18,20,22'
INSERT Testdata SELECT 2,  8, '17,19'
INSERT Testdata SELECT 3,  7, '13,19,20'
INSERT Testdata SELECT 4,  6, ''
INSERT Testdata SELECT 9, 11, '1,2,3,4'

La requête

;WITH tmp(SomeID, OtherID, DataItem, String) AS
(
    SELECT
        SomeID,
        OtherID,
        LEFT(String, CHARINDEX(',', String + ',') - 1),
        STUFF(String, 1, CHARINDEX(',', String + ','), '')
    FROM Testdata
    UNION all

    SELECT
        SomeID,
        OtherID,
        LEFT(String, CHARINDEX(',', String + ',') - 1),
        STUFF(String, 1, CHARINDEX(',', String + ','), '')
    FROM tmp
    WHERE
        String > ''
)

SELECT
    SomeID,
    OtherID,
    DataItem
FROM tmp
ORDER BY SomeID
-- OPTION (maxrecursion 0)
-- normally recursion is limited to 100. If you know you have very long
-- strings, uncomment the option

Production

 SomeID | OtherID | DataItem 
--------+---------+----------
 1      | 9       | 18       
 1      | 9       | 20       
 1      | 9       | 22       
 2      | 8       | 17       
 2      | 8       | 19       
 3      | 7       | 13       
 3      | 7       | 19       
 3      | 7       | 20       
 4      | 6       |          
 9      | 11      | 1        
 9      | 11      | 2        
 9      | 11      | 3        
 9      | 11      | 4        

1
Le code ne fonctionne pas si le changement du type de données de la colonne Datade varchar(max)à varchar(4000), par exemple create table Testdata(SomeID int, OtherID int, Data varchar(4000))?
ca9163d9

4
@NickW cela peut être dû au fait que les pièces avant et après UNION ALL renvoient différents types à partir de la fonction LEFT. Personnellement, je ne vois pas pourquoi vous ne sauteriez pas au MAX une fois que vous auriez atteint 4000 ...
RichardTheKiwi

Pour un GRAND ensemble de valeurs, cela peut dépasser les limites de récursivité pour les CTE.
dsz

3
@dsz C'est à ce moment que vous utilisezOPTION (maxrecursion 0)
RichardTheKiwi

14
Les fonctions LEFT peuvent nécessiter un CAST pour fonctionner .... par exemple LEFT (CAST (Data AS VARCHAR (MAX)) ....
smoore4

141

Enfin, l'attente est terminée avec SQL Server 2016 . Ils ont introduit la fonction de chaîne Split, STRING_SPLIT:

select OtherID, cs.Value --SplitData
from yourtable
cross apply STRING_SPLIT (Data, ',') cs

Toutes les autres méthodes pour diviser une chaîne comme XML, table Tally, boucle while, etc. ont été supprimées par cette STRING_SPLITfonction.

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

Pour les versions antérieures, l' utilisation de la table de pointage est une fonction de chaîne fractionnée (meilleure approche possible)

CREATE FUNCTION [dbo].[DelimitedSplit8K]
        (@pString VARCHAR(8000), @pDelimiter CHAR(1))
RETURNS TABLE WITH SCHEMABINDING AS
 RETURN
--===== "Inline" CTE Driven "Tally Table" produces values from 0 up to 10,000...
     -- enough to cover NVARCHAR(4000)
  WITH E1(N) AS (
                 SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL 
                 SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL 
                 SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
                ),                          --10E+1 or 10 rows
       E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
       E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
 cteTally(N) AS (--==== This provides the "base" CTE and limits the number of rows right up front
                     -- for both a performance gain and prevention of accidental "overruns"
                 SELECT TOP (ISNULL(DATALENGTH(@pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
                ),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
                 SELECT 1 UNION ALL
                 SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(@pString,t.N,1) = @pDelimiter
                ),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
                 SELECT s.N1,
                        ISNULL(NULLIF(CHARINDEX(@pDelimiter,@pString,s.N1),0)-s.N1,8000)
                   FROM cteStart s
                )
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
 SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
        Item       = SUBSTRING(@pString, l.N1, l.L1)
   FROM cteLen l
;

Renvoyé de Tally OH! Une fonction SQL 8K «CSV Splitter» améliorée


9
réponse très importante
Syed Md. Kamruzzaman

J'utiliserais STRING_SPLIT si seulement le serveur était sur SQL Server 2016! BTW selon la page à laquelle vous êtes lié, le nom du champ qu'il génère n'est valuepas SplitData.
Stewart

89

Vérifie ça

 SELECT A.OtherID,  
     Split.a.value('.', 'VARCHAR(100)') AS Data  
 FROM  
 (
     SELECT OtherID,  
         CAST ('<M>' + REPLACE(Data, ',', '</M><M>') + '</M>' AS XML) AS Data  
     FROM  Table1
 ) AS A CROSS APPLY Data.nodes ('/M') AS Split(a); 

8
Lorsque vous utilisez cette approche, vous devez vous assurer qu'aucune de vos valeurs ne contient quelque chose qui serait du
code

C'est bien. Puis-je vous demander, comment pourrais-je réécrire cela si je voulais que la nouvelle colonne affiche uniquement le premier caractère de ma chaîne fractionnée?
Contrôle le

Cela a parfaitement fonctionné, merci! J'ai dû mettre à jour la limite VARCHAR mais cela a fonctionné après cela.
chazbot7

Je dois vous dire que cette méthode est "affectueuse" (vous sentez l'amour?) Appelée la "méthode XML Splitter" et est presque aussi lente qu'une boucle While ou un CTE récursif. Je vous recommande fortement de l'éviter à tout moment. Utilisez plutôt DelimitedSplit8K. Il souffle les portes de tout sauf de la fonction Split_String () en 2016 ou d'un CLR bien écrit.
Jeff Moden

20
select t.OtherID,x.Kod
    from testData t
    cross apply (select Code from dbo.Split(t.Data,',') ) x

3
Fait exactement ce que je recherchais, et plus facile à lire que la plupart des autres exemples (à condition qu'il y ait déjà une fonction dans la base de données pour le fractionnement de chaîne délimité). En tant que personne qui ne connaissait pas auparavant CROSS APPLY, c'est un peu utile!
tobriand

Je ne pouvais pas comprendre cette partie (sélectionnez Code dans dbo.Split (t.Data, ','))? dbo.Split est une table où est-ce que cela existe et aussi le code est la colonne dans la table fractionnée? Je n'ai pas pu trouver la liste de ces tables ou valeurs n'importe où dans cette page?
Jayendran

1
Mon code de travail est:select t.OtherID, x.* from testData t cross apply (select item as Data from dbo.Split(t.Data,',') ) x
Akbar Kautsar

12

En février 2016 - voir l'exemple de tableau TALLY - très susceptible de surpasser mon TVF ci-dessous, à partir de février 2014. Conserver le post d'origine ci-dessous pour la postérité:


Trop de code répété à mon goût dans les exemples ci-dessus. Et je n'aime pas les performances des CTE et XML. En outre, une explicite Idafin que les consommateurs spécifiques à une commande puissent spécifier une ORDER BYclause.

CREATE FUNCTION dbo.Split
(
    @Line nvarchar(MAX),
    @SplitOn nvarchar(5) = ','
)
RETURNS @RtnValue table
(
    Id INT NOT NULL IDENTITY(1,1) PRIMARY KEY CLUSTERED,
    Data nvarchar(100) NOT NULL
)
AS
BEGIN
    IF @Line IS NULL RETURN

    DECLARE @split_on_len INT = LEN(@SplitOn)
    DECLARE @start_at INT = 1
    DECLARE @end_at INT
    DECLARE @data_len INT

    WHILE 1=1
    BEGIN
        SET @end_at = CHARINDEX(@SplitOn,@Line,@start_at)
        SET @data_len = CASE @end_at WHEN 0 THEN LEN(@Line) ELSE @end_at-@start_at END
        INSERT INTO @RtnValue (data) VALUES( SUBSTRING(@Line,@start_at,@data_len) );
        IF @end_at = 0 BREAK;
        SET @start_at = @end_at + @split_on_len
    END

    RETURN
END

6

C'est bien de voir que cela a été résolu dans la version 2016, mais pour tous ceux qui n'y figurent pas, voici deux versions généralisées et simplifiées des méthodes ci-dessus.

La méthode XML est plus courte, mais nécessite bien sûr la chaîne pour permettre l'astuce xml (pas de «mauvais» caractères.)

Méthode XML:

create function dbo.splitString(@input Varchar(max), @Splitter VarChar(99)) returns table as
Return
    SELECT Split.a.value('.', 'VARCHAR(max)') AS Data FROM
    ( SELECT CAST ('<M>' + REPLACE(@input, @Splitter, '</M><M>') + '</M>' AS XML) AS Data 
    ) AS A CROSS APPLY Data.nodes ('/M') AS Split(a); 

Méthode récursive:

create function dbo.splitString(@input Varchar(max), @Splitter Varchar(99)) returns table as
Return
  with tmp (DataItem, ix) as
   ( select @input  , CHARINDEX('',@Input)  --Recu. start, ignored val to get the types right
     union all
     select Substring(@input, ix+1,ix2-ix-1), ix2
     from (Select *, CHARINDEX(@Splitter,@Input+@Splitter,ix+1) ix2 from tmp) x where ix2<>0
   ) select DataItem from tmp where ix<>0

Fonction en action

Create table TEST_X (A int, CSV Varchar(100));
Insert into test_x select 1, 'A,B';
Insert into test_x select 2, 'C,D';

Select A,data from TEST_X x cross apply dbo.splitString(x.CSV,',') Y;

Drop table TEST_X

XML-METHOD 2: Unicode Friendly 😀 (avec l'aimable autorisation de Max Hodges) create function dbo.splitString(@input nVarchar(max), @Splitter nVarchar(99)) returns table as Return SELECT Split.a.value('.', 'NVARCHAR(max)') AS Data FROM ( SELECT CAST ('<M>' + REPLACE(@input, @Splitter, '</M><M>') + '</M>' AS XML) AS Data ) AS A CROSS APPLY Data.nodes ('/M') AS Split(a);


1
Cela peut sembler évident, mais comment utilisez-vous ces deux fonctions? Surtout, pouvez-vous montrer comment l'utiliser dans le cas d'utilisation de l'OP?
jpaugh

1
Voici un exemple rapide: Créez la table TEST_X (A int, CSV Varchar (100)); Insérez dans test_x sélectionnez 1, 'A, B'; Insérez dans test_x sélectionnez 2, 'C, D'; Sélectionnez A, les données de TEST_X x appliquent en croix dbo.splitString (x.CSV, ',') Y; Table basse TEST_X
Eske Rahn

C'est exactement ce dont j'avais besoin! Je vous remercie.
Nitin Badole

5

Veuillez vous référer ci-dessous TSQL. La fonction STRING_SPLIT n'est disponible qu'au niveau de compatibilité 130 et supérieur.

TSQL:

DECLARE @stringValue NVARCHAR(400) = 'red,blue,green,yellow,black'  
DECLARE @separator CHAR = ','

SELECT [value]  As Colour
FROM STRING_SPLIT(@stringValue, @separator); 

RÉSULTAT:

Couleur

rouge bleu vert jaune noir


5

Très tard, mais essayez ceci:

SELECT ColumnID, Column1, value  --Do not change 'value' name. Leave it as it is.
FROM tbl_Sample  
CROSS APPLY STRING_SPLIT(Tags, ','); --'Tags' is the name of column containing comma separated values

Nous avions donc ceci: tbl_Sample:

ColumnID|   Column1 |   Tags
--------|-----------|-------------
1       |   ABC     |   10,11,12    
2       |   PQR     |   20,21,22

Après avoir exécuté cette requête:

ColumnID|   Column1 |   value
--------|-----------|-----------
1       |   ABC     |   10
1       |   ABC     |   11
1       |   ABC     |   12
2       |   PQR     |   20
2       |   PQR     |   21
2       |   PQR     |   22

Merci!


STRING_SPLITest astucieux mais il nécessite SQL Server 2016. docs.microsoft.com/en-us/sql/t-sql/functions/…
Craig Silver

solution élégante.
Sangram Nandkhile

3
DECLARE @id_list VARCHAR(MAX) = '1234,23,56,576,1231,567,122,87876,57553,1216'
DECLARE @table TABLE ( id VARCHAR(50) )
DECLARE @x INT = 0
DECLARE @firstcomma INT = 0
DECLARE @nextcomma INT = 0

SET @x = LEN(@id_list) - LEN(REPLACE(@id_list, ',', '')) + 1 -- number of ids in id_list

WHILE @x > 0
    BEGIN
        SET @nextcomma = CASE WHEN CHARINDEX(',', @id_list, @firstcomma + 1) = 0
                              THEN LEN(@id_list) + 1
                              ELSE CHARINDEX(',', @id_list, @firstcomma + 1)
                         END
        INSERT  INTO @table
        VALUES  ( SUBSTRING(@id_list, @firstcomma + 1, (@nextcomma - @firstcomma) - 1) )
        SET @firstcomma = CHARINDEX(',', @id_list, @firstcomma + 1)
        SET @x = @x - 1
    END

SELECT  *
FROM    @table

C'est l'une des rares méthodes qui fonctionne avec la prise en charge SQL limitée dans Azure SQL Data Warehouse.
Aaron Schultz

1
;WITH tmp(SomeID, OtherID, DataItem, Data) as (
    SELECT SomeID, OtherID, LEFT(Data, CHARINDEX(',',Data+',')-1),
        STUFF(Data, 1, CHARINDEX(',',Data+','), '')
FROM Testdata
WHERE Data > ''
)
SELECT SomeID, OtherID, Data
FROM tmp
ORDER BY SomeID

avec seulement une toute petite modification à la requête ci-dessus ...


6
Pouvez-vous expliquer brièvement en quoi il s'agit d'une amélioration par rapport à la version de la réponse acceptée?
Leigh

Pas d'union tous ... moins de code. Puisqu'il utilise union all au lieu de union, cela ne devrait pas être une différence de performance?
TamusJRoyce

1
Cela n'a pas renvoyé toutes les lignes qu'il devrait contenir. Je ne sais pas ce que les données nécessitent l'union tous, mais votre solution a renvoyé le même nombre de lignes que la table d'origine.
Oedhel Setren

1
(le problème ici est que la partie récursive est celle omise ...)
Eske Rahn

Ne pas me donner la sortie attendue ne donnant que le premier record consécutif
Ankit Misra

1

Lorsque vous utilisez cette approche, vous devez vous assurer qu'aucune de vos valeurs ne contient quelque chose qui serait du code XML illégal - user1151923

J'utilise toujours la méthode XML. Assurez-vous d'utiliser XML VALIDE. J'ai deux fonctions pour convertir entre XML valide et texte. (J'ai tendance à supprimer les retours de chariot car je n'en ai généralement pas besoin.

CREATE FUNCTION dbo.udf_ConvertTextToXML (@Text varchar(MAX)) 
    RETURNS varchar(MAX)
AS
    BEGIN
        SET @Text = REPLACE(@Text,CHAR(10),'')
        SET @Text = REPLACE(@Text,CHAR(13),'')
        SET @Text = REPLACE(@Text,'<','&lt;')
        SET @Text = REPLACE(@Text,'&','&amp;')
        SET @Text = REPLACE(@Text,'>','&gt;')
        SET @Text = REPLACE(@Text,'''','&apos;')
        SET @Text = REPLACE(@Text,'"','&quot;')
    RETURN @Text
END


CREATE FUNCTION dbo.udf_ConvertTextFromXML (@Text VARCHAR(MAX)) 
    RETURNS VARCHAR(max)
AS
    BEGIN
        SET @Text = REPLACE(@Text,'&lt;','<')
        SET @Text = REPLACE(@Text,'&amp;','&')
        SET @Text = REPLACE(@Text,'&gt;','>')
        SET @Text = REPLACE(@Text,'&apos;','''')
        SET @Text = REPLACE(@Text,'&quot;','"')
    RETURN @Text
END

1
Il y a un petit problème avec le code que vous avez là. Cela changera '<' en '& amp; lt;' au lieu de «& lt;» comme il se doit. Vous devez donc d'abord coder «&».
Stewart

Il n'y a pas besoin d'une telle fonction ... Utilisez simplement les capacités implicites. Essayez ceci:SELECT (SELECT '<&> blah' + CHAR(13)+CHAR(10) + 'next line' FOR XML PATH(''))
Shnugo

1

Fonction

CREATE FUNCTION dbo.SplitToRows (@column varchar(100), @separator varchar(10))
RETURNS @rtnTable TABLE
  (
  ID int identity(1,1),
  ColumnA varchar(max)
  )
 AS
BEGIN
    DECLARE @position int = 0
    DECLARE @endAt int = 0
    DECLARE @tempString varchar(100)

    set @column = ltrim(rtrim(@column))

    WHILE @position<=len(@column)
    BEGIN       
        set @endAt = CHARINDEX(@separator,@column,@position)
            if(@endAt=0)
            begin
            Insert into @rtnTable(ColumnA) Select substring(@column,@position,len(@column)-@position)
            break;
            end
        set @tempString = substring(ltrim(rtrim(@column)),@position,@endAt-@position)

        Insert into @rtnTable(ColumnA) select @tempString
        set @position=@endAt+1;
    END
    return
END

Cas d'utilisation

select * from dbo.SplitToRows('T14; p226.0001; eee; 3554;', ';')

Ou simplement une sélection avec plusieurs jeux de résultats

DECLARE @column varchar(max)= '1234; 4748;abcde; 324432'
DECLARE @separator varchar(10) = ';'
DECLARE @position int = 0
DECLARE @endAt int = 0
DECLARE @tempString varchar(100)

set @column = ltrim(rtrim(@column))

WHILE @position<=len(@column)
BEGIN       
    set @endAt = CHARINDEX(@separator,@column,@position)
        if(@endAt=0)
        begin
        Select substring(@column,@position,len(@column)-@position)
        break;
        end
    set @tempString = substring(ltrim(rtrim(@column)),@position,@endAt-@position)

    select @tempString
    set @position=@endAt+1;
END

L'utilisation d'une boucle while dans une fonction de valeur de table à plusieurs états est à peu près la pire façon possible de diviser des chaînes. Il y a déjà tellement d'options basées sur des ensembles sur cette question.
Sean Lange

0

Ci-dessous fonctionne sur SQL Server 2008

select *, ROW_NUMBER() OVER(order by items) as row# 
from 
( select 134 myColumn1, 34 myColumn2, 'd,c,k,e,f,g,h,a' comaSeperatedColumn) myTable
    cross apply 
SPLIT (rtrim(comaSeperatedColumn), ',') splitedTable -- gives 'items'  column 

Obtiendra tous les produits cartésiens avec les colonnes de la table d'origine plus les "éléments" de la table fractionnée.


0

Vous pouvez utiliser la fonction suivante pour extraire des données

CREATE FUNCTION [dbo].[SplitString]
(    
    @RowData NVARCHAR(MAX),
    @Delimeter NVARCHAR(MAX)
)
RETURNS @RtnValue TABLE 
(
    ID INT IDENTITY(1,1),
    Data NVARCHAR(MAX)
) 
AS
BEGIN 
    DECLARE @Iterator INT
    SET @Iterator = 1

    DECLARE @FoundIndex INT
    SET @FoundIndex = CHARINDEX(@Delimeter,@RowData)

    WHILE (@FoundIndex>0)
    BEGIN
        INSERT INTO @RtnValue (data)
        SELECT 
            Data = LTRIM(RTRIM(SUBSTRING(@RowData, 1, @FoundIndex - 1)))

        SET @RowData = SUBSTRING(@RowData,
                @FoundIndex + DATALENGTH(@Delimeter) / 2,
                LEN(@RowData))

        SET @Iterator = @Iterator + 1
        SET @FoundIndex = CHARINDEX(@Delimeter, @RowData)
    END

    INSERT INTO @RtnValue (Data)
    SELECT Data = LTRIM(RTRIM(@RowData))

    RETURN
END

L'utilisation d'une boucle while dans une fonction de valeur de table à plusieurs états est à peu près la pire façon possible de diviser des chaînes. Il y a déjà tellement d'options basées sur des ensembles sur cette question.
Sean Lange
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.