328 votes

Transformer une chaîne de caractères séparée par des virgules en lignes individuelles

J'ai une table SQL comme ceci :

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

existe-t-il une requête où je peux effectuer une requête du type SELECT OtherID, SplitData WHERE SomeID = 'abcdef-.......' qui renvoie des lignes individuelles, comme ceci :

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

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

Je suis conscient que le stockage d'un comma-separated dans une base de données relationnelle semble stupide, mais le cas d'utilisation normal dans l'application du consommateur rend cela vraiment utile.

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

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

0 votes

318voto

RichardTheKiwi Points 58121

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


Exemple de table :

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

Sortie

 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 votes

Le code ne fonctionne pas si on change le type de données de la colonne. Data de varchar(max) a varchar(4000) par exemple create table Testdata(SomeID int, OtherID int, Data varchar(4000)) ?

4 votes

@NickW cela peut être dû au fait que les parties avant et après UNION ALL renvoient des types différents de la fonction LEFT. Personnellement, je ne vois pas pourquoi vous ne passeriez pas à MAX une fois que vous aurez atteint 4000...

0 votes

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

102voto

bvr Points 2551

Vérifiez ceci

 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);

10 votes

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

0 votes

C'est génial. Puis-je vous demander comment je réécrirais cela si je voulais que la nouvelle colonne n'affiche que le premier caractère de ma chaîne fractionnée ?

1 votes

Je dois vous dire que cette méthode, appelée "méthode du séparateur XML", est presque aussi lente qu'une boucle While ou qu'un CTE récursif. Je vous recommande vivement de l'éviter à tout moment. Utilisez DelimitedSplit8K à la place. Il surpasse tout, sauf la fonction Split_String() de 2016 ou un CLR bien écrit.

23voto

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

3 votes

Fait exactement ce que je cherchais, et plus facile à lire que beaucoup d'autres exemples (à condition qu'il y ait déjà une fonction dans la base de données pour la division des chaînes délimitées). Pour quelqu'un qui n'était pas familier avec CROSS APPLY c'est plutôt utile !

0 votes

Je n'ai pas compris cette partie (select Code from dbo.Split(t.Data,',') ) ? dbo.Split est une table où cela existe et aussi Code La colonne est dans la table Split ? Je n'ai pas pu trouver la liste de ces tables ou valeurs dans cette page ?

1 votes

Mes codes de travail sont : select t.OtherID, x.* from testData t cross apply (select item as Data from dbo.Split(t.Data,',') ) x

15voto

dsz Points 506

En février 2016 - voir l'exemple de la table TALLY - très susceptible de surperformer mon TVF ci-dessous, de février 2014. Je garde le post original 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 du XML. De plus, un Id afin que les consommateurs qui sont spécifiques à une commande puissent spécifier une ORDER BY clause.

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;

6voto

Jayvee Points 2399
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;

1 votes

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X