Virgülle Ayrılmış bir dizeyi tek tek satırlara dönüştürme


234

Böyle bir SQL tablo var:

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

böyle SELECT OtherID, SplitData WHERE SomeID = 'abcdef-.......'tek tek satırları döndürür gibi bir sorgu gerçekleştirebilirsiniz bir sorgu var :

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

Temel olarak virgüldeki verilerimi tek tek satırlara mı bölüyorum?

comma-separatedİlişkisel bir veritabanına bir dize saklamanın aptalca geldiğinin farkındayım , ancak tüketici uygulamasındaki normal kullanım durumu bunu gerçekten yararlı kılıyor.

Çağrıya ihtiyaç duyduğum için uygulamada bölünmeyi yapmak istemiyorum, bu yüzden tüm uygulamayı yeniden düzenlemeden önce seçenekleri keşfetmek istedim.

It SQL Server 2008(non-R2).


Yanıtlar:


265

SQL Server'dan harika özyinelemeli işlevleri kullanabilirsiniz:


Örnek tablo:

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'

Sorgu

;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

Çıktı

 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 çalışmıyor değişim sütununun veri türü ise Datagelen varchar(max)etmek varchar(4000)örn create table Testdata(SomeID int, OtherID int, Data varchar(4000))?
ca9163d9

4
@NickW bunun nedeni, UNION ALL'dan önceki ve sonraki parçaların SOL işlevinden farklı türler döndürmesi olabilir. Şahsen 4000'e
ulaştıktan

BÜYÜK bir değer kümesi için bu, CTE'ler için özyineleme sınırlarını aşabilir.
dsz

3
@dsz İşte o zamanOPTION (maxrecursion 0)
RichardTheKiwi

14
SOL işlevlerin çalışması için bir CAST gerekebilir ... örneğin SOL (CAST (VERİ AS VARCHAR (MAX)) ....
smoore4

141

Son olarak, bekleme SQL Server 2016 ile bitti . Split string fonksiyonunu tanıttılar STRING_SPLIT:

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

XML, Tally tablosu, while döngüsü, vb. Gibi dizeyi bölmek için diğer tüm yöntemler bu STRING_SPLITişlev tarafından uçurulmuştur .

İşte performans karşılaştırması ile mükemmel bir makale: Performans Sürprizleri ve Varsayımlar: STRING_SPLIT .

Daha eski sürümler için, burada çizelge tablosunu kullanmak bir bölünmüş dize işlevidir (mümkün olan en iyi yaklaşım)

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
;

Yönlendirilen Tally OH! Geliştirilmiş SQL 8K “CSV Splitter” İşlevi


9
çok önemli cevap
Syed Md. Kamruzzaman

Yalnızca sunucu SQL Server 2016'da olsaydı STRING_SPLIT kullanırdım! BTW bağlandığınız sayfaya göre çıktısını alan adı valuedeğil SplitData.
Stewart

89

Şuna göz at

 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
Bu yaklaşımı kullanırken değerlerinizin hiçbirinin yasadışı XML olabilecek bir şey içermediğinden emin olmalısınız
user1151923

Bu harika. Size sorabilir miyim, eğer yeni sütunun sadece bölünmüş dizemdeki ilk karakteri göstermesini istersem nasıl yeniden yazabilirim?
Kontrol

Bu mükemmel çalıştı, teşekkürler! VARCHAR limitini güncellemem gerekti ama bundan sonra mükemmel çalıştı.
chazbot7

Size "XML Splitter Metodu" denilen ve "While Loop" veya "Yinelenen CTE" gibi yavaş bir yöntem "lovingl" (sevgi hissediyorum?) Olduğunu söylemek zorundayım. Her zaman kaçınmanızı şiddetle tavsiye ederim. Bunun yerine DelimitedSplit8K kullanın. 2016'daki Split_String () işlevi veya iyi yazılmış bir CLR dışındaki kapıları patlatır.
Jeff Moden

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

3
Tam olarak ne oldu sonra ve diğer örneklerin çoğundan daha kolay yapar (zaten ayrılmış bir bölünmüş dize bölünmesi için DB bir işlevi varsa). Daha önce tanımadığınız biri olarak CROSS APPLY, bu biraz yararlı!
tobriand

Bu bölümü anlayamadım (dbo.Split (t.Data, ',') Kodunu seçin)? dbo.Split , bu var olan bir tablo ve ayrıca Split sütundaki kod nedir? Bu sayfanın herhangi bir yerinde bu tabloların veya değerlerin listesini bulamadım?
Jayendran

1
Çalışma kodum:select t.OtherID, x.* from testData t cross apply (select item as Data from dbo.Split(t.Data,',') ) x
Akbar Kautsar

12

Şubat 2016'dan itibaren - TALLY Tablo Örneğine bakınız - Şubat 2014'ten itibaren TVF'mden daha iyi performans gösterme olasılığı yüksektir.


Yukarıdaki örneklerde beğenme için çok fazla tekrarlanan kod. CTE'lerin ve XML'in performansından hoşlanmıyorum. Ayrıca, Idsiparişe özgü tüketicilerin bir ORDER BYcümle belirleyebilmesi için açık bir ifade .

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

2016 sürümünde çözüldüğünü görmek güzel, ancak bunun üzerinde olmayanlar için, yukarıdaki yöntemlerin iki genelleştirilmiş ve basitleştirilmiş versiyonu.

XML yöntemi daha kısadır, ancak elbette xml-trickine izin vermek için dizeyi gerektirir ('kötü' karakter içermez.)

XML Yöntem:

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

Özyinelemeli yöntem:

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

Görevdeki işlev

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 YÖNTEM 2: Unicode Dostu 😀 (Max Hodges'ın eki ile ) 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
Bu açık görünebilir, ancak bu iki işlevi nasıl kullanıyorsunuz? Özellikle, OP'nin kullanım durumunda nasıl kullanılacağını gösterebilir misiniz?
jpaugh

1
İşte kısa bir örnek: Tablo oluştur TEST_X (A int, CSV Varchar (100)); Test_x'e ekle seçin 1, 'A, B'; Test_x'e 2, 'C, D' seçin; A'yı seçin, TEST_X x çaprazındaki veriler dbo.splitString (x.CSV, ',') Y'yi uygulayın; Tabloyu düşür TEST_X
Eske Rahn

Tam da ihtiyacım olan şey bu! Teşekkür ederim.
Nitin Badole

5

Lütfen aşağıdaki TSQL'e bakın. STRING_SPLIT işlevi yalnızca 130 ve üstü uyumluluk düzeyinde kullanılabilir.

TSQL:

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

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

SONUÇ:

Renk

kırmızı mavi yeşil sarı siyah


5

Çok geç ama şunu deneyin:

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

Yani bunu yapıyorduk: tbl_Sample:

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

Bu sorguyu çalıştırdıktan sonra:

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

Teşekkürler!


STRING_SPLITşık ama SQL Server 2016 gerektiriyor. docs.microsoft.com/en-us/sql/t-sql/functions/…
Craig Silver

zarif çözüm.
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

Bu, Azure SQL Veri Ambarı'ndaki sınırlı SQL desteğiyle çalışan birkaç yöntemden biridir.
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

yukarıdaki sorgu sadece küçük küçük değişiklik ile ...


6
Bunun, kabul edilen cevaptaki versiyona göre nasıl bir gelişme olduğunu kısaca anlatabilir misiniz?
Leigh

Sendika yok ... daha az kod. Sendika yerine sendika kullandığından performans farkı olmamalı mı?
TamusJRoyce

1
Bu, sahip olması gereken tüm satırları döndürmedi. Verilerin ne olduğunu birliğin gerektirdiğinden emin değilim, ancak çözümünüz orijinal tabloyla aynı sayıda satır döndürdü.
Oedhel Setren

1
(Buradaki sorun, özyinelemeli kısmın atlandığı bölümdür ...)
Eske Rahn

Bana sadece ayrı satırda ilk rekoru veren beklenen çıktı vermiyor
Ankit Misra

1

Bu yaklaşımı kullanırken değerlerinizin hiçbirinin yasadışı XML olabilecek bir şey içermediğinden emin olmalısınız - user1151923

Her zaman XML yöntemini kullanırım. VALID XML kullandığınızdan emin olun. Geçerli XML ve Metin arasında dönüştürmek için iki işlevi var. (Genellikle onlara ihtiyacım olmadığından taşıma iadelerini çıkarma eğilimindeyim.

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
Oradaki kodla ilgili küçük bir sorun var. '<' İle '& amp; lt;' '& lt;' yerine olması gerektiği gibi. Bu yüzden önce '&' kodlamanız gerekir.
Stewart

Böyle bir işleve gerek yoktur ... Sadece örtük yetenekleri kullanın. SELECT (SELECT '<&> blah' + CHAR(13)+CHAR(10) + 'next line' FOR XML PATH(''))
Şunu

1

fonksiyon

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

Kullanım örneği

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

Veya yalnızca birden fazla sonuç kümesine sahip bir seçim

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

Çok aşamalı tablo değerli bir işlev içinde while döngüsü kullanmak, dizeleri ayırmak için mümkün olan en kötü yoldur. Bu soru üzerinde zaten çok sayıda set tabanlı seçenek var.
Sean Lange

0

Aşağıda sql server 2008 üzerinde çalışıyor

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 

Orijin tablo sütunları artı bölünmüş tablonun "öğeleri" ile tüm Kartezyen ürün alacak.


0

Verileri ayıklamak için aşağıdaki işlevi kullanabilirsiniz

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

Çok aşamalı tablo değerli bir işlev içinde while döngüsü kullanmak, dizeleri ayırmak için mümkün olan en kötü yoldur. Bu soru üzerinde zaten çok sayıda set tabanlı seçenek var.
Sean Lange
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.