Trasformare una stringa separata da virgola in singole righe


234

Ho una tabella SQL come questa:

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

c'è una query in cui posso eseguire una query come SELECT OtherID, SplitData WHERE SomeID = 'abcdef-.......'quella che restituisce singole righe, in questo modo:

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

Fondamentalmente dividere i miei dati alla virgola in singole righe?

Sono consapevole che archiviare una comma-separatedstringa in un database relazionale sembra stupido, ma il normale caso d'uso nell'applicazione consumer lo rende davvero utile.

Non voglio eseguire la suddivisione nell'applicazione in quanto ho bisogno del paging, quindi volevo esplorare le opzioni prima di refactoring dell'intera app.

È SQL Server 2008(non R2).


Risposte:


265

È possibile utilizzare le meravigliose funzioni ricorsive da SQL Server:


Tabella di esempio:

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 domanda

;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

Produzione

 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
Il codice non funziona se si cambia il tipo di dati della colonna Datada varchar(max)a varchar(4000), ad esempio create table Testdata(SomeID int, OtherID int, Data varchar(4000))?
ca9163d9,

4
@NickW ciò può essere dovuto al fatto che le parti prima e dopo UNION ALL restituiscono tipi diversi dalla funzione LEFT. Personalmente non vedo perché non
salteresti

Per un GRANDE set di valori, questo può superare i limiti di ricorsione per CTE.
dsz,

3
@dsz Ecco quando usiOPTION (maxrecursion 0)
RichardTheKiwi il

14
Le funzioni SINISTRA potrebbero richiedere un CAST per funzionare .... ad esempio SINISTRA (CAST (Data AS VARCHAR (MAX)) ....
smoore4

141

Infine, l'attesa è terminata con SQL Server 2016 . Essi hanno introdotto la funzione di stringa Split, STRING_SPLIT:

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

Tutti gli altri metodi per dividere stringhe come XML, tabella Tally, mentre loop, ecc. Sono stati spazzati via da questa STRING_SPLITfunzione.

Ecco un eccellente articolo con confronto delle prestazioni: Sorprese e ipotesi sulle prestazioni: STRING_SPLIT .

Per le versioni precedenti, usando la tabella tally qui c'è una funzione di stringa divisa (il miglior approccio possibile)

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
;

Riferito da Tally OH! Una funzione "CSV Splitter" SQL 8K migliorata


9
risposta molto importante
Syed Md. Kamruzzaman

Userei STRING_SPLIT se solo il server fosse su SQL Server 2016! A proposito in base alla pagina che hai collegato, il nome del campo che emette è value, no SplitData.
Stewart,

89

Controllare questo

 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
Quando usi questo approccio devi assicurarti che nessuno dei tuoi valori contenga qualcosa che sarebbe XML illegale
user1151923

Questo è fantastico Posso chiederti come riscriverei se volessi che la nuova colonna mostrasse solo il primo carattere della mia stringa divisa?
Controlla l'

Questo ha funzionato perfettamente, grazie! Ho dovuto aggiornare il limite VARCHAR ma dopo ha funzionato perfettamente.
chazbot7,

Devo dirti che il metodo è "amorevole" (senti l'amore?) Chiamato "Metodo Splitter XML" ed è lento quasi quanto un ciclo While o un CTE ricorsivo. Consiglio vivamente di evitarlo in ogni momento. Utilizzare invece DelimitedSplit8K. Spazza via le porte di tutto tranne la funzione Split_String () nel 2016 o un CLR ben scritto.
Jeff Moden l'

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

3
Fa esattamente quello che stavo cercando e più facile da leggere rispetto a molti altri esempi (a condizione che nel DB sia già presente una funzione per la suddivisione delle stringhe delimitata). Come qualcuno che non conosceva in precedenza CROSS APPLY, questo è un po 'utile!
tobriand,

Non riesco a capire questa parte (selezionare il codice da dbo.Split (t.Data, ','))? dbo.Split è una tabella dove esiste e anche Code è la colonna nella tabella divisa? non sono riuscito a trovare l'elenco di tali tabelle o valori in nessuna parte di questa pagina?
Jayendran,

1
I miei codici di lavoro sono:select t.OtherID, x.* from testData t cross apply (select item as Data from dbo.Split(t.Data,',') ) x
Akbar Kautsar l'

12

A partire da febbraio 2016 - vedi l'esempio della tabella TALLY - molto probabilmente supererà il mio TVF di seguito, a partire da febbraio 2014. Mantenere i post originali di seguito per i posteri:


Troppo codice ripetuto per i miei gusti negli esempi precedenti. E non mi piacciono le prestazioni di CTE e XML. Inoltre, un esplicito in Idmodo che i consumatori specifici dell'ordine possano specificare una ORDER BYclausola.

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

Bello vedere che è stato risolto nella versione 2016, ma per tutti quelli che non lo sono, ecco due versioni generalizzate e semplificate dei metodi sopra.

Il metodo XML è più breve, ma ovviamente richiede la stringa per consentire il trucco xml (nessun carattere "cattivo").

XML-Metodo:

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

Metodo ricorsivo:

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

Funzione in azione

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 😀 (aggiunta per gentile concessione di 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
Questo può sembrare ovvio, ma come si usano queste due funzioni? In particolare, puoi mostrare come usarlo nel caso d'uso del PO?
jpaugh,

1
Ecco un breve esempio: Crea tabella TEST_X (A int, CSV Varchar (100)); Inserisci in test_x seleziona 1, 'A, B'; Inserisci in test_x seleziona 2, 'C, D'; Seleziona A, i dati da TEST_X x cross applicano dbo.splitString (x.CSV, ',') Y; Drop table TEST_X
Eske Rahn,

Questo è esattamente quello di cui avevo bisogno! Grazie.
Nitin Badole,

5

Si prega di fare riferimento di seguito a TSQL. La funzione STRING_SPLIT è disponibile solo con il livello di compatibilità 130 e superiore.

TSQL:

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

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

RISULTATO:

Colore

rosso blu verde giallo nero


5

Molto tardi, ma prova questo:

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

Quindi stavamo avendo questo: tbl_Sample:

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

Dopo aver eseguito questa query:

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

Grazie!


STRING_SPLITè elegante ma richiede SQL Server 2016. docs.microsoft.com/en-us/sql/t-sql/functions/…
Craig Silver

soluzione elegante.
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

Questo è uno dei pochi metodi che funziona con il supporto SQL limitato in 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

con solo una piccola piccola modifica alla query sopra ...


6
Puoi spiegare brevemente come si tratta di un miglioramento rispetto alla versione nella risposta accettata?
Leigh,

Nessuna unione tutto ... meno codice. Dal momento che utilizza union all invece di union, non dovrebbe esserci una differenza di prestazioni?
TamusJRoyce,

1
Questo non ha restituito tutte le righe che dovrebbe avere. Non sono sicuro dei dati che richiedono l'unione totale, ma la tua soluzione ha restituito lo stesso numero di righe della tabella originale.
Oedhel Setren,

1
(il problema qui è che la parte ricorsiva è quella omessa ...)
Eske Rahn,

Non dandomi il risultato atteso solo dando il primo record in fila separata
Ankit Misra

1

Quando usi questo approccio devi assicurarti che nessuno dei tuoi valori contenga qualcosa che sarebbe XML illegale - user1151923

Uso sempre il metodo XML. Assicurati di utilizzare VALID XML. Ho due funzioni da convertire tra XML e Text validi. (Tendo a eliminare i ritorni a capo del carrello poiché di solito non ne ho bisogno.

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
C'è un piccolo problema con il codice che hai lì. Cambierà '<' in '& amp; lt;' anziché "& lt;" come dovrebbe. Quindi devi prima codificare "&".
Stewart,

Non è necessaria una tale funzione ... Usa solo le abilità implicite. Prova questo:SELECT (SELECT '<&> blah' + CHAR(13)+CHAR(10) + 'next line' FOR XML PATH(''))
Shnugo,

1

Funzione

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

Caso d'uso

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

O solo una selezione con più set di risultati

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'utilizzo di un ciclo while all'interno di una funzione con valori di tabella multistatement è quasi il modo peggiore per dividere le stringhe. Ci sono già così tante opzioni basate su set su questa domanda.
Sean Lange,

0

Di seguito funziona su 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 

Ottiene tutti i prodotti cartesiani con le colonne della tabella di origine più "articoli" della tabella divisa.


0

È possibile utilizzare la seguente funzione per estrarre i dati

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'utilizzo di un ciclo while all'interno di una funzione con valori di tabella multistatement è quasi il modo peggiore per dividere le stringhe. Ci sono già così tante opzioni basate su set su questa domanda.
Sean Lange,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.