Mengubah string Dipisahkan Koma menjadi baris individual


234

Saya memiliki Tabel SQL seperti ini:

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

apakah ada kueri di mana saya dapat melakukan kueri seperti SELECT OtherID, SplitData WHERE SomeID = 'abcdef-.......'yang mengembalikan baris individual, seperti ini:

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

Pada dasarnya membagi data saya di koma menjadi baris individual?

Saya sadar bahwa menyimpan comma-separatedstring ke dalam basis data relasional terdengar bodoh, tetapi kasus penggunaan normal dalam aplikasi konsumen membuatnya sangat membantu.

Saya tidak ingin melakukan pemisahan dalam aplikasi karena saya perlu paging, jadi saya ingin menjelajahi opsi sebelum refactoring seluruh aplikasi.

Ini SQL Server 2008(non-R2).


Jawaban:


265

Anda dapat menggunakan fungsi rekursif yang luar biasa dari SQL Server:


Tabel sampel:

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'

Kueri

;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

Keluaran

 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
Kode tidak berfungsi jika mengubah tipe data kolom Datadari varchar(max)menjadi varchar(4000), misalnya create table Testdata(SomeID int, OtherID int, Data varchar(4000))?
ca9163d9

4
@NickW ini mungkin karena bagian sebelum dan sesudah UNION ALL mengembalikan tipe berbeda dari fungsi KIRI. Secara pribadi saya tidak melihat mengapa Anda tidak akan melompat ke MAX setelah Anda mencapai 4000 ...
RichardTheKiwi

Untuk sekumpulan nilai BESAR, ini dapat melampaui batas rekursi untuk CTE.
dsz

3
@dsz Saat itulah Anda menggunakanOPTION (maxrecursion 0)
RichardTheKiwi

14
Fungsi LEFT mungkin membutuhkan CAST untuk berfungsi .... misalnya LEFT (CAST (Data AS VARCHAR (MAX)) ....
smoore4

141

Akhirnya, penantian berakhir dengan SQL Server 2016 . Mereka telah memperkenalkan fungsi string yang Split, STRING_SPLIT:

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

Semua metode lain untuk memisahkan string seperti XML, Tally table, while, dll. Telah terpesona oleh STRING_SPLITfungsi ini .

Inilah artikel yang sangat bagus dengan perbandingan kinerja: Kejutan Kinerja dan Asumsi: STRING_SPLIT .

Untuk versi yang lebih lama, menggunakan tabel penghitungan di sini adalah satu fungsi string split (pendekatan terbaik)

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
;

Disebut dari Tally OH! Fungsi SQL 8K “CSV Splitter” yang Ditingkatkan


9
jawaban yang sangat penting
Syed Md. Kamruzzaman

Saya akan menggunakan STRING_SPLIT jika hanya servernya yang menggunakan SQL Server 2016! BTW menurut halaman yang telah Anda tautkan, nama bidang yang dihasilkannya value, bukan SplitData.
Stewart

89

Periksa ini

 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
Saat menggunakan pendekatan ini, Anda harus memastikan bahwa tidak ada nilai yang berisi sesuatu yang akan menjadi XML ilegal
user1151923

Ini bagus. Dapatkah saya bertanya kepada Anda, bagaimana saya menulis ulang bahwa jika saya ingin kolom baru hanya menampilkan karakter pertama dari string split saya?
Kontrol

Ini bekerja dengan baik, terima kasih! Saya harus memperbarui batas VARCHAR tetapi itu berhasil setelah itu.
chazbot7

Saya harus memberi tahu Anda bahwa metode "penuh kasih" (rasakan cinta?) Disebut "Metode XML Splitter" dan hampir selambat salah satu Loop Sementara atau CTE Rekursif. Saya sangat menyarankan Anda menghindarinya setiap saat. Gunakan DelimitedSplit8K sebagai gantinya. Ini menghancurkan semua hal kecuali fungsi Split_String () pada tahun 2016 atau CLR yang ditulis dengan baik.
Jeff Moden

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

3
Melakukan persis apa yang saya kejar, dan lebih mudah dibaca daripada banyak contoh lainnya (asalkan sudah ada fungsi dalam DB untuk pemisahan string yang dibatasi). Sebagai seseorang yang sebelumnya tidak terbiasa CROSS APPLY, itu agak berguna!
tobriand

Saya tidak dapat memahami bagian ini (pilih Kode dari dbo.Split (t.Data, ','))? dbo.Split adalah tabel di mana ini ada dan juga Kode adalah Kolom di tabel Split? saya tidak dapat menemukan daftar tabel atau nilai di mana saja di Halaman ini?
Jayendran

1
Kode kerja saya adalah:select t.OtherID, x.* from testData t cross apply (select item as Data from dbo.Split(t.Data,',') ) x
Akbar Kautsar

12

Mulai Februari 2016 - lihat Contoh Tabel TALLY - sangat mungkin mengungguli TVF saya di bawah ini, mulai Februari 2014. Menyimpan posting asli di bawah ini untuk anak cucu:


Terlalu banyak kode berulang untuk seleraku dalam contoh di atas. Dan saya tidak suka kinerja CTE dan XML. Juga, eksplisit Idsehingga konsumen yang spesifik pesanan dapat menentukan ORDER BYklausa.

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

Senang melihat bahwa itu telah diselesaikan dalam versi 2016, tetapi untuk semua yang tidak pada itu, di sini adalah dua versi umum dan disederhanakan dari metode di atas.

Metode XML lebih pendek, tetapi tentu saja membutuhkan string untuk memungkinkan xml-trick (tidak ada karakter 'buruk'.)

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

Metode rekursif:

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

Berfungsi dalam aksi

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 😀 (Tambahan milik 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
Ini mungkin tampak jelas, tetapi bagaimana Anda menggunakan dua fungsi ini? Terutama, dapatkah Anda menunjukkan cara menggunakannya dalam use case OP?
jpaugh

1
Ini adalah contoh cepat: Buat tabel TEST_X (Int, CSV Varchar (100)); Masukkan ke test_x pilih 1, 'A, B'; Masukkan ke test_x pilih 2, 'C, D'; Pilih A, data dari TEST_X x silang berlaku dbo.splitString (x.CSV, ',') Y; Drop table TEST_X
Eske Rahn

Inilah yang saya butuhkan! Terima kasih.
Nitin Badole

5

Silakan merujuk di bawah TSQL. Fungsi STRING_SPLIT hanya tersedia di bawah level kompatibilitas 130 ke atas.

TSQL:

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

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

HASIL:

Warna

merah biru hijau kuning hitam


5

Sangat terlambat tetapi cobalah ini:

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

Jadi kami mengalami ini: tbl_Sample:

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

Setelah menjalankan kueri ini:

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

Terima kasih!


STRING_SPLITbagus tetapi membutuhkan SQL Server 2016. docs.microsoft.com/en-us/sql/t-sql/functions/…
Craig Silver

solusi elegan.
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

Ini adalah salah satu dari beberapa metode yang bekerja dengan dukungan SQL terbatas di 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

dengan hanya sedikit modifikasi kecil ke kueri di atas ...


6
Bisakah Anda menjelaskan secara singkat bagaimana ini merupakan peningkatan dari versi dalam jawaban yang diterima?
Leigh

Tidak ada penyatuan semua ... kode kurang. Karena itu menggunakan serikat semua bukan serikat, bukankah seharusnya perbedaan kinerja?
TamusJRoyce

1
Ini tidak mengembalikan semua baris yang seharusnya. Saya tidak yakin bagaimana dengan data yang membutuhkan penyatuan semua, tetapi solusi Anda mengembalikan jumlah baris yang sama dengan tabel asli.
Oedhel Setren

1
(Masalahnya di sini adalah bahwa bagian rekursif adalah yang dihilangkan ...)
Eske Rahn

Tidak memberi saya hasil yang diharapkan hanya memberikan catatan pertama dalam baris terpisah
Ankit Misra

1

Saat menggunakan pendekatan ini, Anda harus memastikan bahwa tidak ada nilai Anda yang mengandung sesuatu yang ilegal XML - user1151923

Saya selalu menggunakan metode XML. Pastikan Anda menggunakan VALID XML. Saya memiliki dua fungsi untuk mengkonversi antara XML dan Teks yang valid. (Saya cenderung menghapus carriage return karena saya biasanya tidak membutuhkannya.

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
Ada masalah kecil dengan kode yang Anda miliki di sana. Ini akan berubah '<' menjadi '& amp; lt;' alih-alih '& lt;' seperti seharusnya. Jadi, Anda harus menyandikan '&' terlebih dahulu.
Stewart

Tidak perlu fungsi seperti itu ... Cukup gunakan kemampuan implisit. Coba ini:SELECT (SELECT '<&> blah' + CHAR(13)+CHAR(10) + 'next line' FOR XML PATH(''))
Shnugo

1

Fungsi

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

Gunakan kasing

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

Atau hanya pilih dengan hasil beberapa set

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

Menggunakan loop sementara di dalam fungsi multistatement table dihargai hanya tentang cara terburuk untuk membagi string. Ada begitu banyak opsi berdasarkan set pada pertanyaan ini.
Sean Lange

0

Di bawah ini berfungsi pada 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 

Akan mendapatkan semua produk Cartesian dengan kolom tabel asal plus "item" dari tabel split.


0

Anda dapat menggunakan fungsi berikut untuk mengekstrak data

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

Menggunakan loop sementara di dalam fungsi multistatement table dihargai hanya tentang cara terburuk untuk membagi string. Ada begitu banyak opsi berdasarkan set pada pertanyaan ini.
Sean Lange
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.