쉼표로 구분 된 문자열을 개별 행으로 변환


234

다음과 같은 SQL 테이블이 있습니다.

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

다음과 같이 SELECT OtherID, SplitData WHERE SomeID = 'abcdef-.......'개별 행을 반환 하는 쿼리를 수행 할 수있는 쿼리가 있습니까?

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

기본적으로 쉼표의 데이터를 개별 행으로 분할합니까?

comma-separated관계형 데이터베이스에 문자열 을 저장하면 바보 같은 소리가 들리지만 소비자 응용 프로그램의 일반적인 사용 사례가 실제로 도움이됩니다.

페이징이 필요할 때 응용 프로그램에서 분할을하고 싶지 않으므로 전체 응용 프로그램을 리팩토링하기 전에 옵션을 탐색하고 싶었습니다.

그것은의 SQL Server 2008(비 R2).


답변:


265

SQL Server에서 멋진 재귀 함수를 사용할 수 있습니다.


샘플 테이블 :

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'

쿼리

;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

산출

 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
코드는 작동하지 않습니다 변화 컬럼의 데이터 유형의 경우 Data에서 varchar(max)varchar(4000)예를 들어, create table Testdata(SomeID int, OtherID int, Data varchar(4000))?
ca9163d9

4
@NickW UNION ALL 전후의 파트가 LEFT 함수와 다른 유형을 반환하기 때문일 수 있습니다. 당신이 MAX로 이동하지 왜 개인적으로 나는 당신이 ... 4000에 도착하면 표시되지 않습니다
RichardTheKiwi

BIG 값 세트의 경우 CTE의 재귀 한계를 초과 할 수 있습니다.
dsz

3
@dsz 바로 그 때입니다OPTION (maxrecursion 0)
RichardTheKiwi

14
LEFT 함수가 작동하려면 CAST가 필요할 수 있습니다. 예를 들어 LEFT (CAST (Data AS VARCHAR (MAX)) ....
smoore4

141

마지막으로 SQL Server 2016 에서 대기가 끝났습니다 . 그들은 문자열 분리 기능을 도입했습니다 STRING_SPLIT:

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

XML, Tally 테이블, while 루프 등과 같은 문자열을 분할하는 다른 모든 방법은이 STRING_SPLIT함수에 의해 사라졌습니다 .

여기에 성능 비교와 훌륭한 기사입니다 성능 놀라움과 가정 : STRING_SPLIT .

이전 버전의 경우 Tally Table을 사용하면 하나의 분할 문자열 함수가 있습니다 (최상의 접근 방식)

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
;

Tally OH 에서 추천 ! 향상된 SQL 8K“CSV 스플리터”기능


9
매우 중요한 답변
Syed Md. Kamruzzaman

서버 만 SQL Server 2016에있는 경우 STRING_SPLIT을 사용합니다! BTW 당신이 연결 한 페이지에 따르면,이 출력 필드의 이름입니다 value하지 SplitData.
스튜어트

89

이것을 확인하십시오

 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
이 방법을 사용할 때는 잘못된 XML을 포함하는 값이 값에 포함되어 있지 않아야합니다.
user1151923

대단하다. 새 열에 내 분할 문자열의 첫 번째 문자 만 표시하도록하려면 어떻게 다시 작성할 수 있습니까?
컨트롤

이것은 완벽하게 작동했습니다, 감사합니다! VARCHAR 제한을 업데이트해야했지만 그 후 성능이 작동했습니다.
chazbot7

이 방법은 "XML Splitter Method"라고하는 "lovingl"(사랑을 느끼십니까?)이며 While 루프 나 재귀 CTE만큼 느립니다. 항상 피하는 것이 좋습니다. 대신 DelimitedSplit8K를 사용하십시오. 2016 년 Split_String () 함수 또는 잘 작성된 CLR을 제외한 모든 것의 문을 날려 버립니다.
Jeff Moden

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

3
내가 겪은 것을 정확하게하고 다른 많은 예제보다 읽기 쉽습니다 (구분 된 문자열 분할을 위해 DB에 이미 함수가있는 경우). 이전에 익숙하지 않은 사람으로서 CROSS APPLY유용합니다!
tobriand

이 부분을 이해할 수 없었습니다 (dbo.Split (t.Data, ',')에서 Code 선택)? dbo.Split 은 이것이 존재하는 테이블이며 코드 는 분할 테이블의 열입니까? 이 페이지 어디에서나 해당 테이블 또는 값 목록을 찾을 수 없습니까?
Jayendran 2016 년

1
내 작업 코드는 다음과 같습니다select t.OtherID, x.* from testData t cross apply (select item as Data from dbo.Split(t.Data,',') ) x
Akbar Kautsar

12

2016 년 2 월 기준-TALLY 테이블 예를 참조하십시오. 2014 년 2 월부터 아래 TVF보다 성능이 우수 할 가능성이 높습니다.


위의 예제에서 좋아하는 반복 코드가 너무 많습니다. 그리고 저는 CTE와 XML의 성능을 싫어합니다. 또한 Id주문 별 소비자가 ORDER BY절을 지정할 수 있도록 명시 적 입니다.

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 버전에서 해결되었음을 알았지 만 그 위에없는 모든 사람들을 위해 위의 방법에 대한 두 가지 일반화되고 단순화 된 버전이 있습니다.

XML 방법은 더 짧지 만 물론 xml-trick을 허용하는 문자열이 필요합니다 ( 'bad'문자 없음).

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

재귀 방법 :

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

작동중인 기능

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 : 유니 코드 사용 가능 😀 (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
이것은 분명해 보이지만이 두 기능을 어떻게 사용합니까? 특히 OP 사용 사례에서 사용 방법을 보여줄 수 있습니까?
jpaugh

1
다음은 간단한 예입니다. 테이블 TEST_X (A int, CSV Varchar (100)); test_x에 삽입 1, 'A, B'를 선택하십시오. test_x에 삽입 2, 'C, D'를 선택하십시오. A, TEST_X x의 데이터를 적용하고 교차 적용 dbo.splitString (x.CSV, ',') Y; Drop TEST_X
Eske Rahn

이것이 바로 내가 필요한 것입니다! 감사합니다.
Nitin Badole

5

아래 TSQL을 참조하십시오. STRING_SPLIT 기능은 호환성 수준 130 이상에서만 사용할 수 있습니다.

TSQL :

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

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

결과:

색깔

빨강 파랑 녹색 노랑 검정


5

매우 늦었지만 시도해보십시오.

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

그래서 우리는 이것을 가지고 있습니다 : tbl_Sample :

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

이 쿼리를 실행 한 후 :

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

감사!


STRING_SPLIT멋지
Craig Silver

우아한 솔루션.
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

Azure SQL Data Warehouse에서 제한된 SQL 지원으로 작동하는 몇 가지 방법 중 하나입니다.
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

위의 쿼리를 약간만 수정하면 ...


6
수락 된 답변의 버전보다 이것이 어떻게 개선되는지 간략하게 설명 할 수 있습니까?
Leigh

모든 노조가 ... 코드가 적습니다. 유니온 대신 유니온을 모두 사용하기 때문에 성능 차이가 없어야합니까?
TamusJRoyce

1
이것은 필요한 모든 행을 반환하지 않았습니다. 데이터에 대해 통합이 필요한 것이 무엇인지 확실하지 않지만 솔루션은 원래 테이블과 동일한 수의 행을 반환했습니다.
Oedhel Setren

1
(여기서 문제는 재귀 부분이 생략 된 것입니다 ...)
Eske Rahn

별도의 행에서 첫 번째 레코드 만 제공하는 예상 출력을 제공하지 않음
Ankit Misra

1

이 방법을 사용할 때는 잘못된 XML을 포함하는 값이 값에 포함되어 있지 않아야합니다. – user1151923

나는 항상 XML 방법을 사용합니다. 올바른 XML을 사용해야합니다. 유효한 XML과 텍스트를 변환하는 두 가지 기능이 있습니다. (일반적으로 필요하지 않으므로 캐리지 리턴을 제거하는 경향이 있습니다.

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
거기에있는 코드에 작은 문제가 있습니다. '<'를 '& lt;'로 변경합니다. '& lt;'대신 그래야합니다. 따라서 '&'를 먼저 인코딩해야합니다.
스튜어트

그런 기능이 필요하지 않습니다 ... 암시 적 능력 만 사용하십시오. – 사용해보십시오 :SELECT (SELECT '<&> blah' + CHAR(13)+CHAR(10) + 'next line' FOR XML PATH(''))
Shnugo

1

함수

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

사용 사례

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

또는 여러 결과 집합이있는 선택

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

다중 문 테이블 값 함수 내에서 while 루프를 사용하는 것은 문자열을 분할하는 가장 나쁜 방법입니다. 이 질문에는 이미 많은 세트 기반 옵션이 있습니다.
Sean Lange

0

아래는 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 

원본 테이블 열에 분할 테이블의 "항목"을 더한 모든 카티 전 곱을 얻습니다.


0

다음 기능을 사용하여 데이터를 추출 할 수 있습니다

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

다중 문 테이블 값 함수 내에서 while 루프를 사용하는 것은 문자열을 분할하는 가장 나쁜 방법입니다. 이 질문에는 이미 많은 세트 기반 옵션이 있습니다.
Sean Lange
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.