Przekształcanie łańcucha rozdzielanego przecinkami na pojedyncze rzędy


234

Mam taką tabelę SQL:

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

czy istnieje zapytanie, w którym mogę wykonać takie zapytanie, SELECT OtherID, SplitData WHERE SomeID = 'abcdef-.......'które zwraca pojedyncze wiersze, takie jak to:

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

Zasadniczo podzielisz moje dane przecinkiem na poszczególne wiersze?

Wiem, że przechowywanie comma-separatedłańcucha w relacyjnej bazie danych brzmi głupio, ale normalny przypadek użycia w aplikacji konsumenckiej sprawia, że ​​jest to naprawdę pomocne.

Nie chcę dokonywać podziału w aplikacji, ponieważ potrzebuję stronicowania, więc chciałem zbadać opcje przed refaktoryzacją całej aplikacji.

To SQL Server 2008(nie R2).


Odpowiedzi:


265

Możesz użyć wspaniałych funkcji rekurencyjnych z SQL Server:


Przykładowa tabela:

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'

Zapytanie

;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

Wynik

 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
Kod nie działa, jeśli zmienisz typ danych kolumny Dataz varchar(max)na varchar(4000)np. create table Testdata(SomeID int, OtherID int, Data varchar(4000))?
ca9163d9

4
@NickW może to być spowodowane tym, że części przed i po UNION ALL zwracają różne typy z funkcji LEWEJ. Osobiście nie rozumiem, dlaczego nie skoczyłbyś na MAX, gdy dojdziesz do 4000 ...
RichardTheKiwi

W przypadku DUŻEGO zestawu wartości może to przekroczyć limity rekurencji dla CTE.
dsz

3
@dsz To kiedy używaszOPTION (maxrecursion 0)
RichardTheKiwi

14
Funkcje LEFT mogą wymagać CAST do działania .... na przykład LEFT (CAST (Data AS VARCHAR (MAX)) ....
smoore4

141

Wreszcie oczekiwanie dobiegło końca dzięki SQL Server 2016 . Wprowadzili funkcję Split string STRING_SPLIT:

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

Wszystkie inne metody dzielenia ciągów znaków, takie jak XML, tabela Tally, pętla while itp. Zostały zdmuchnięte przez tę STRING_SPLITfunkcję.

Oto doskonały artykuł z porównaniem wydajności: Niespodzianki i założenia dotyczące wydajności: STRING_SPLIT .

W starszych wersjach użycie tabeli podsumowań jest jedną funkcją podziału łańcucha (najlepsze możliwe podejście)

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
;

Polecony przez Tally OH! Ulepszona funkcja „CSV Splitter” SQL 8K


9
bardzo ważna odpowiedź
Syed MD Kamruzzaman

Użyłbym STRING_SPLIT, gdyby tylko serwer był na SQL Server 2016! BTW według strony, z którą się łączysz, nazwa pola, które wyprowadza, to valuenie SplitData.
Stewart

89

Sprawdź to

 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
Korzystając z tego podejścia, musisz upewnić się, że żadna z Twoich wartości nie zawiera czegoś, co byłoby nielegalne XML
1151923

To jest świetne. Czy mogę cię zapytać, jak mam przepisać, że jeśli nowa kolumna ma pokazywać tylko pierwszy znak z podzielonego ciągu?
Kontrola

To działało idealnie, dziękuję! Musiałem zaktualizować limit VARCHAR, ale potem zadziałał.
chazbot7

Muszę powiedzieć, że metoda ta to „lovingl” (czujesz miłość?) Zwana „XML Splitter Method” i jest prawie tak wolna, jak pętla While lub rekurencyjna CTE. Zdecydowanie zalecamy unikanie tego przez cały czas. Zamiast tego użyj DelimitedSplit8K. Wysadza drzwi ze wszystkiego oprócz funkcji Split_String () w 2016 roku lub dobrze napisanego CLR.
Jeff Moden

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

3
Robi dokładnie to, o co mi chodziło i jest łatwiejszy do odczytania niż wiele innych przykładów (pod warunkiem, że w DB jest już funkcja podziału rozdzielonego łańcucha). Dla kogoś, kto wcześniej nie był zaznajomiony CROSS APPLY, jest to przydatne!
tobriand

Nie mogłem zrozumieć tej części (wybierz Kod z dbo.Split (t.Data, ','))? dbo.Split to tabela, w której to istnieje, a także Code to Column in Split table? nie mogłem znaleźć listy tych tabel lub wartości w żadnym miejscu na tej stronie?
Jayendran

1
Mój działający kod to:select t.OtherID, x.* from testData t cross apply (select item as Data from dbo.Split(t.Data,',') ) x
Akbar Kautsar,

12

W lutym 2016 r. - patrz przykład tabeli TALLY - najprawdopodobniej przewyższy moją TVF poniżej, od lutego 2014 r. Zachowanie oryginalnego postu dla potomności:


Zbyt dużo powtarzającego się kodu dla moich upodobań w powyższych przykładach. I nie podoba mi się wydajność CTE i XML. Również wyraźne Id, aby konsumenci, którzy są specyficzni dla zamówienia, mogli określić ORDER BYklauzulę.

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

Miło widzieć, że zostało to rozwiązane w wersji 2016, ale dla wszystkich tych, których nie ma, oto dwie uogólnione i uproszczone wersje powyższych metod.

Metoda XML jest krótsza, ale oczywiście wymaga ciągu, aby zezwolić na sztuczkę xml (bez „złych” znaków).

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

Metoda rekurencyjna:

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

Funkcja w akcji

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 😀 (Dodatek udostępniony dzięki uprzejmości Maxa Hodgesa) 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
Może się to wydawać oczywiste, ale jak korzystać z tych dwóch funkcji? W szczególności, czy możesz pokazać, jak go używać w przypadku użycia PO?
jpaugh

1
Oto szybki przykład: Utwórz tabelę TEST_X (A int, CSV Varchar (100)); Wstaw do test_x wybierz 1, „A, B”; Wstaw do test_x wybierz 2, „C, D”; Wybierz A, dane z TEST_X x krzyż zastosuj dbo.splitString (x.CSV, ',') Y; Zrzuć stół TEST_X
Eske Rahn

Właśnie tego potrzebowałem! Dziękuję Ci.
Nitin Badole

5

Zobacz poniżej TSQL. Funkcja STRING_SPLIT jest dostępna tylko na poziomie zgodności 130 i wyższym.

TSQL:

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

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

WYNIK:

Kolor

czerwony niebieski zielony żółty czarny


5

Bardzo późno, ale wypróbuj to:

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

Mieliśmy to: tbl_Sample:

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

Po uruchomieniu tego zapytania:

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

Dzięki!


STRING_SPLITjest fajny, ale wymaga SQL Server 2016. docs.microsoft.com/en-us/sql/t-sql/functions/...
Craig Silver

eleganckie rozwiązanie.
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

Jest to jedna z niewielu metod, która działa z ograniczoną obsługą SQL w usłudze 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

z niewielką modyfikacją powyższego zapytania ...


6
Czy potrafisz krótko wyjaśnić, w jaki sposób jest to poprawa w stosunku do wersji w zaakceptowanej odpowiedzi?
Leigh,

Brak unii wszystkie ... mniej kodu. Ponieważ używa unii wszystko zamiast unii, nie powinno być różnicy w wydajności?
TamusJRoyce,

1
Nie zwróciło to wszystkich wierszy, które powinno mieć. Nie jestem pewien, co z danymi wymaga unii wszystkich, ale twoje rozwiązanie zwróciło taką samą liczbę wierszy jak oryginalna tabela.
Oedhel Setren

1
(problem polega na tym, że część rekurencyjna została pominięta ...)
Eske Rahn

Nie dając mi oczekiwanego rezultatu, dając tylko pierwszy rekord w osobnym rzędzie
Ankit Misra

1

Korzystając z tego podejścia, musisz upewnić się, że żadna z twoich wartości nie zawiera czegoś, co byłoby nielegalne XML - user1151923

Zawsze używam metody XML. Upewnij się, że używasz VALID XML. Mam dwie funkcje do konwersji między poprawnym XML a tekstem. (Staram się rozbierać zwroty karetki, ponieważ zwykle ich nie potrzebuję.

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
Jest mały problem z kodem, który tam masz. Zmieni „<” na „& amp; lt;” zamiast „& lt;” tak jak powinno. Najpierw musisz zakodować „&”.
Stewart

Nie ma takiej potrzeby ... Po prostu użyj ukrytych zdolności. Wypróbuj to:SELECT (SELECT '<&> blah' + CHAR(13)+CHAR(10) + 'next line' FOR XML PATH(''))
Shnugo,

1

Funkcjonować

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

Przypadek użycia

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

Lub po prostu wybierz z wieloma zestawami wyników

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

Korzystanie z pętli while w funkcji wycenianej w tabeli wielopłaszczyznowej jest najgorszym możliwym sposobem podziału ciągów. Jest już tyle opcji opartych na zestawie dla tego pytania.
Sean Lange,

0

Poniżej działa na serwerze SQL 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 

Otrzyma cały produkt kartezjański z kolumnami tabeli początkowej oraz „elementami” podzielonej tabeli.


0

Możesz użyć następującej funkcji do wyodrębnienia danych

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

Korzystanie z pętli while w funkcji wycenianej w tabeli wielopłaszczyznowej jest najgorszym możliwym sposobem podziału ciągów. Jest już tyle opcji opartych na zestawie dla tego pytania.
Sean Lange,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.