Durch Komma getrennte Zeichenfolge in einzelne Zeilen umwandeln


234

Ich habe eine SQL-Tabelle wie diese:

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

Gibt es eine Abfrage, bei der ich eine solche Abfrage ausführen kann, SELECT OtherID, SplitData WHERE SomeID = 'abcdef-.......'die einzelne Zeilen wie folgt zurückgibt:

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

Grundsätzlich meine Daten am Komma in einzelne Zeilen aufteilen?

Ich bin mir bewusst, dass das Speichern einer comma-separatedZeichenfolge in einer relationalen Datenbank dumm klingt, aber der normale Anwendungsfall in der Consumer-Anwendung macht dies wirklich hilfreich.

Ich möchte die Aufteilung in der Anwendung nicht durchführen, da ich Paging benötige. Daher wollte ich die Optionen untersuchen, bevor ich die gesamte App umgestalte.

Es ist SQL Server 2008(nicht R2).


Antworten:


265

Sie können die wunderbaren rekursiven Funktionen von SQL Server verwenden:


Beispieltabelle:

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'

Die Abfrage

;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

Ausgabe

 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
Der Code funktioniert nicht, wenn der Datentyp der Spalte Datavon varchar(max)auf geändert wird varchar(4000), z create table Testdata(SomeID int, OtherID int, Data varchar(4000)).
ca9163d9

4
@NickW Dies kann daran liegen, dass die Teile vor und nach UNION ALL unterschiedliche Typen von der LEFT-Funktion zurückgeben. Persönlich verstehe ich nicht, warum Sie nicht zu MAX springen würden, wenn Sie 4000 erreicht haben ...
RichardTheKiwi

Bei einem BIG-Wertesatz kann dies die Rekursionsgrenzen für CTEs überschreiten.
dsz

3
@dsz Das ist, wenn Sie verwendenOPTION (maxrecursion 0)
RichardTheKiwi

14
Die LEFT-Funktionen benötigen möglicherweise ein CAST, um zu funktionieren .... zum Beispiel LEFT (CAST (Daten AS VARCHAR (MAX)) ....
smoore4

141

Mit SQL Server 2016 ist das Warten endlich vorbei . Sie haben die Split-String-Funktion eingeführt STRING_SPLIT:

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

Alle anderen Methoden zum Teilen von Zeichenfolgen wie XML, Tally-Tabelle, while-Schleife usw. wurden von dieser STRING_SPLITFunktion weggeblasen .

Hier ist ein ausgezeichneter Artikel mit Leistungsvergleich: Leistungsüberraschungen und Annahmen: STRING_SPLIT .

Bei älteren Versionen ist die Verwendung der Tally-Tabelle hier eine Split-String-Funktion (bestmöglicher Ansatz).

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
;

Empfohlen von Tally OH! Eine verbesserte SQL 8K "CSV Splitter" -Funktion


9
sehr wichtige Antwort
Syed Md. Kamruzzaman

Ich würde STRING_SPLIT verwenden, wenn nur der Server auf SQL Server 2016 wäre! Übrigens, je nach der Seite, auf die Sie verlinkt haben, lautet der ausgegebene Feldname valuenicht SplitData.
Stewart

89

Überprüfen Sie dies

 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
Wenn Sie diesen Ansatz verwenden, müssen Sie sicherstellen, dass keiner Ihrer Werte etwas enthält, das illegales XML wäre
user1151923

Das ist toll. Kann ich Sie fragen, wie würde ich das umschreiben, wenn ich wollte, dass in der neuen Spalte nur das erste Zeichen aus meiner geteilten Zeichenfolge angezeigt wird?
Kontrolle

Das hat perfekt funktioniert, danke! Ich musste das VARCHAR-Limit aktualisieren, aber danach funktionierte es perfekt.
Chazbot7

Ich muss Ihnen sagen, dass die Methode "Lieben" (die Liebe fühlen?) Genannt wird, die als "XML-Splitter-Methode" bezeichnet wird und fast so langsam ist wie eine While-Schleife oder ein rekursiver CTE. Ich empfehle dringend, dass Sie es jederzeit vermeiden. Verwenden Sie stattdessen DelimitedSplit8K. Es sprengt die Türen von allem außer der Split_String () -Funktion im Jahr 2016 oder einer gut geschriebenen CLR.
Jeff Moden

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

3
Tut genau das, wonach ich gesucht habe, und ist leichter zu lesen als viele andere Beispiele (vorausgesetzt, es gibt bereits eine Funktion in der Datenbank für die getrennte Aufteilung von Zeichenfolgen). Als jemand, den man vorher nicht kennt CROSS APPLY, ist das irgendwie nützlich!
tobriand

Ich konnte diesen Teil nicht verstehen (wählen Sie Code aus dbo.Split (t.Data, ','))? dbo.Split ist eine Tabelle, in der diese vorhanden ist, und Code ist die Spalte in der geteilten Tabelle? Ich konnte die Liste dieser Tabellen oder Werte nirgendwo auf dieser Seite finden.
Jayendran

1
Mein select t.OtherID, x.* from testData t cross apply (select item as Data from dbo.Split(t.Data,',') ) x
Arbeitscode

12

Ab Februar 2016 - siehe das TALLY-Tabellenbeispiel - ist es sehr wahrscheinlich, dass mein TVF ab Februar 2014 übertroffen wird. Den ursprünglichen Beitrag unten für die Nachwelt behalten:


Zu viel wiederholter Code für meinen Geschmack in den obigen Beispielen. Und ich mag die Leistung von CTEs und XML nicht. Auch eine explizite, Iddamit Verbraucher, die auftragsspezifisch sind, eine ORDER BYKlausel angeben können .

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

Schön zu sehen, dass es in der Version 2016 gelöst wurde, aber für alle, die dies nicht tun, sind hier zwei verallgemeinerte und vereinfachte Versionen der oben genannten Methoden.

Die XML-Methode ist kürzer, erfordert aber natürlich die Zeichenfolge, um den XML-Trick zuzulassen (keine 'schlechten' Zeichen).

XML-Methode:

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

Rekursive Methode:

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

Funktion in Aktion

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-METHODE 2: Unicode-freundlich 😀 (Ergänzung mit freundlicher Genehmigung von 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
Dies mag offensichtlich erscheinen, aber wie verwenden Sie diese beiden Funktionen? Können Sie insbesondere zeigen, wie es im Anwendungsfall des OP verwendet wird?
Jpaugh

1
Hier ein kurzes Beispiel: Erstellen Sie die Tabelle TEST_X (A int, CSV Varchar (100)); In test_x einfügen wählen Sie 1, 'A, B'; In test_x einfügen wählen Sie 2, 'C, D'; Wählen Sie A, Daten aus TEST_X x cross anwenden dbo.splitString (x.CSV, ',') Y; Drop Tabelle TEST_X
Eske Rahn

Genau das brauchte ich! Danke dir.
Nitin Badole

5

Bitte beziehen Sie sich unten auf TSQL. Die Funktion STRING_SPLIT ist nur unter der Kompatibilitätsstufe 130 und höher verfügbar.

TSQL:

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

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

ERGEBNIS:

Farbe

rot blau grün gelb schwarz


5

Sehr spät, aber probieren Sie es aus:

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

Also hatten wir folgendes: tbl_Sample:

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

Nach dem Ausführen dieser Abfrage:

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

Vielen Dank!


STRING_SPLITist geschickt, aber es erfordert SQL Server 2016. docs.microsoft.com/en-us/sql/t-sql/functions/…
Craig Silver

elegante Lösung.
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

Dies ist eine der wenigen Methoden, die mit der eingeschränkten SQL-Unterstützung in Azure SQL Data Warehouse funktionieren.
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

mit nur winzigen kleinen Änderungen an der obigen Abfrage ...


6
Können Sie in der akzeptierten Antwort kurz erklären, wie dies eine Verbesserung gegenüber der Version darstellt?
Leigh

Keine Vereinigung alle ... weniger Code. Sollte es kein Leistungsunterschied sein, da Union All anstelle von Union verwendet wird?
TamusJRoyce

1
Dies gab nicht alle Zeilen zurück, die es haben sollte. Ich bin nicht sicher, was die Daten angeht, für die die Vereinigung alle erforderlich ist, aber Ihre Lösung hat die gleiche Anzahl von Zeilen wie die ursprüngliche Tabelle zurückgegeben.
Oedhel Setren

1
(Das Problem hier ist, dass der rekursive Teil derjenige ist, der weggelassen wurde ...)
Eske Rahn

Ich habe nicht die erwartete Ausgabe erhalten, sondern nur die erste Aufzeichnung in einer separaten Reihe
Ankit Misra

1

Wenn Sie diesen Ansatz verwenden, müssen Sie sicherstellen, dass keiner Ihrer Werte etwas enthält, das unzulässiges XML wäre - user1151923

Ich benutze immer die XML-Methode. Stellen Sie sicher, dass Sie VALID XML verwenden. Ich habe zwei Funktionen zum Konvertieren zwischen gültigem XML und Text. (Ich neige dazu, die Wagenrückläufe zu entfernen, da ich sie normalerweise nicht brauche.

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
Es gibt ein kleines Problem mit dem Code, den Sie dort haben. Es wird '<' in '& amp; lt;' anstelle von '& lt;' wie es sollte. Sie müssen also zuerst '&' codieren.
Stewart

Eine solche Funktion ist nicht erforderlich ... Verwenden Sie einfach die impliziten Fähigkeiten. Probieren Sie dies aus:SELECT (SELECT '<&> blah' + CHAR(13)+CHAR(10) + 'next line' FOR XML PATH(''))
Shnugo

1

Funktion

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

Anwendungsfall

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

Oder einfach eine Auswahl mit mehreren Ergebnismengen

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

Die Verwendung einer while-Schleife innerhalb einer Multistatement-Tabellenwertfunktion ist der schlechteste Weg, um Zeichenfolgen zu teilen. Es gibt bereits so viele satzbasierte Optionen für diese Frage.
Sean Lange

0

Unten funktioniert auf 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 

Erhält alle kartesischen Produkte mit den Ursprungs-Tabellenspalten plus "Elementen" der geteilten Tabelle.


0

Mit der folgenden Funktion können Sie Daten extrahieren

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

Die Verwendung einer while-Schleife innerhalb einer Multistatement-Tabellenwertfunktion ist der schlechteste Weg, um Zeichenfolgen zu teilen. Es gibt bereits so viele satzbasierte Optionen für diese Frage.
Sean Lange
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.