カンマ区切りの文字列を個別の行に変換する


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関数から異なるタイプを返すためである可能性があります。個人的には、4000になったらMAXにジャンプしない理由はわかりません...
RichardTheKiwi

大きな値のセットの場合、これはCTEの再帰制限を超過する可能性があります。
dsz 2014年

3
@dsz使用するときOPTION (maxrecursion 0)
RichardTheKiwi

14
LEFT関数が機能するには、CASTが必要な場合があります。例:LEFT(CAST(Data AS VARCHAR(MAX))....
smoore4

141

最後に、待機はSQL Server 2016で終わりました。彼らはSplit string関数を導入しましたSTRING_SPLIT

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

XML、Tallyテーブル、whileループなどの文字列を分割する他のすべてのメソッドは、このSTRING_SPLIT関数によって吹き飛ばされました。

パフォーマンスを比較した優れた記事を次に示します:パフォーマンスの驚きと仮定:STRING_SPLIT

古いバージョンの場合、ここでタリーテーブルを使用すると、1つの分割文字列関数になります(最適な方法)。

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 2017年

サーバーがSQL Server 2016上にある場合は、STRING_SPLITを使用します。ところであなたがリンクしたページによると、それが出力するフィールド名は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 2017年

このメソッドは「XMLスプリッターメソッド」と呼ばれる「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 2015年

この部分を理解できませんでした(dbo.Split(t.Data、 '、')からコードを選択してください)?dbo.Splitはこれが存在するテーブルで、コードは分割テーブルの列ですか?このページのどこにもこれらのテーブルまたは値のリストが見つかりませんでしたか?
Jayendran 2017年

1
私の作業のコードは以下のとおりです。select t.OtherID, x.* from testData t cross apply (select item as Data from dbo.Split(t.Data,',') ) x
アクバル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年のバージョンで解決されたことを確認できて嬉しいですが、それ以外のすべての場合について、上記のメソッドの2つの一般化バージョンと簡略化バージョンを次に示します。

XMLメソッドの方が短いですが、もちろんxml-trickを可能にする文字列が必要です(「不良」文字はありません)。

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:Unicodeフレンドリー😀(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
これは明白に思えるかもしれませんが、これらの2つの関数をどのように使用しますか?特に、OPのユースケースでそれを使用する方法を示すことができますか?
jpaugh 2016

1
簡単な例を次に示します。テーブルTEST_X(A int、CSV Varchar(100))を作成します。test_xにselect 1、 'A、B'を挿入します。test_xにselect 2、 'C、D'を挿入します。TEST_XからA、dataを選択しますx cross apply dbo.splitString(x.CSV、 '、')Y; ドロップテーブル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

ありがとう!



エレガントなソリューション。
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サポートで機能する数少ない方法の1つです。
アーロンシュルツ

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
これが承認された回答のバージョンよりもどのように改善されているかを簡単に説明できますか?
リー

ユニオンは一切ありません...コードが少ないです。unionではなくunion allを使用しているので、パフォーマンスに違いはありませんか?
TamusJRoyce

1
これにより、必要なすべての行が返されませんでした。ユニオンallを必要とするデータについてはわかりませんが、ソリューションでは元のテーブルと同じ数の行が返されました。
Oedhel Setren、2015

1
(ここでの問題は、再帰部分が省略されていることです...)
Eske Rahn

期待どおりの出力が得られず、別の行の最初のレコードしか表示されない
Ankit Misra

1

このアプローチを使用するときは、XMLに違法な値が含まれている値がないことを確認する必要があります– user1151923

私はいつもXMLメソッドを使用しています。必ず有効なXMLを使用してください。有効なXMLとテキストの間で変換する2つの関数があります。(通常は必要ないため、キャリッジリターンを取り除く傾向があります。

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
取得したコードに小さな問題があります。「<」が「&amp; lt;」に変わります 「&lt;」の代わりに それのように。したがって、最初に「&」をエンコードする必要があります。
スチュワート

そのような関数の必要はありません...暗黙の能力を使用してください。これを試してください:SELECT (SELECT '<&> blah' + CHAR(13)+CHAR(10) + 'next line' FOR XML PATH(''))
Shnugo '19

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.