将逗号分隔的字符串转换为单独的行

330
我有一个这样的SQL表格: ```html

I have a SQL Table like this:

SomeID OtherID Data
abcdef-..... cdef123-... 18,20,22
abcdef-..... 4554a24-... 17,19
987654-..... 12324a2-... 13,19,20
``` 有没有一种查询方式,类似于`SELECT OtherID, SplitData WHERE SomeID = 'abcdef-.......'`,返回单独的行,像这样: ```html
OtherID SplitData
cdef123-... 18
cdef123-... 20
cdef123-... 22
4554a24-... 17
4554a24-... 19
``` 基本上将我的数据在逗号处拆分成单独的行吗?
我知道将一个“逗号分隔”的字符串存储到关系数据库中听起来很愚蠢,但在消费者应用程序的正常使用情况下,它确实非常有用。
我不想在应用程序中进行拆分,因为我需要分页,所以我想在重构整个应用程序之前探索选项。
它是SQL Server 2008(非R2)。

参见: https://www.periscopedata.com/blog/splitting-comma-separated-values-in-mysql - Rick James
18个回答

319

您可以使用 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
3
@dsz 当你使用OPTION (maxrecursion 0)时,这是一个用法。 - RichardTheKiwi
18
LEFT函数可能需要进行转换才能正常工作,例如LEFT(CAST(Data AS VARCHAR(MAX))…。 - smoore4
显示剩余2条评论

235

终于,SQL Server 2016 推出了 Split string 函数STRING_SPLIT

select OtherID, cs.Value --SplitData
from yourtable
cross apply STRING_SPLIT (Data, ',') cs
STRING_SPLIT函数已经取代了其他字符串分割方法,如XML、Tally表和while循环等。以下是一篇性能比较优秀的文章:Performance Surprises and Assumptions: STRING_SPLIT
对于旧版本,使用tally表是一种最佳的拆分字符串方法。
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 分割器”函数


如果服务器是SQL Server 2016,我会使用STRING_SPLIT!顺便说一下,根据你提供的页面,它输出的字段名为“value”,而不是“SplitData”。 - Stewart
7
被接受的答案是有效的,但考虑到现在是2021年,这个答案现在应该优先考虑。谢谢你 - SPLIT_STRING正是我正在寻找的。 - Matthew Fritz
如果原始数据中的行在使用STRING_SPLIT拆分时包含了空值(在本问题中为“Data”列),则在使用CROSS APPLY时将在结果中省略这些行(在本问题中为“SplitData”列)。要保留这些行,请使用OUTER APPLY。 - krystof236
需要兼容级别130以及SQL Server 2016。 - Geoff

102

请检查这个

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

10
使用这种方法时,您必须确保您的所有值都不包含任何可能导致非法XML的内容。 - user1151923
这很棒。我可以问一下,如果我想要新列仅显示拆分字符串的第一个字符,我该如何重写它? - Control
1
我必须告诉你,这个方法叫做“XML Splitter Method”,是“lovingl”(感受到爱了吗?),几乎和 While Loop 或者 Recursive CTE 一样慢。我强烈建议你避免使用它,而是使用 DelimitedSplit8K。除了2016年的 Split_String() 函数或者良好编写的 CLR 外,它可以超越其他所有方法。 - Jeff Moden
@JeffModen 这种方法的优点是可以在不允许或不可能创建函数(甚至是临时函数)的环境中运行。 - Eduo
@Eduo - 很好。在任何环境下都可能相对超级慢。;) - Jeff Moden

35

虽然晚了,但还是试试这个:

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

谢谢!


2
STRING_SPLIT很方便,但需要SQL Server 2016。https://learn.microsoft.com/en-us/sql/t-sql/functions/string-split-transact-sql?view=sql-server-2017 - Craig Silver
2
优雅的解决方案。 - Sangram Nandkhile
2
是的,@SangramNandkhile 真的说得很好,这是最优雅的解决方案,不需要声明任何变量,代码注释得很好,这正是我想要的。感谢 Dungeon。 - N Khan
1
这里有很多好的答案,但这个答案很简洁。它还使用行号作为排序方法,使用模数从数组中仅提取特定项,并在字段中列出仅一个记录。这样可以避免重写拆分函数。 - Jamie
精彩的东西 - Dermo909
那么,如果Tags为空的情况呢? - undefined

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

3
完全符合我的预期,并且比许多其他示例更易于阅读(前提是在数据库中已经有用于分隔字符串的函数)。作为之前不熟悉“CROSS APPLY”的人,这很有用! - tobriand
我不理解这部分 (select Code from dbo.Split(t.Data,',') ) ? dbo.Split 是一个表,它在哪里存在,而 Code 是 Split 表中的列吗?我无法在此页面中找到这些表或值的列表。 - Jayendran
1
我的工作代码如下:select t.OtherID, x.* from testData t cross apply (select item as Data from dbo.Split(t.Data,',') ) x - Akbar Kautsar

15

截至2016年2月 - 请参考TALLY表格示例,很可能会优于以下来自2014年2月的我的TVF。 以下是原始帖子,供后人参考:


以上示例中有太多重复的代码,我不喜欢公共表达式和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;

11

很高兴看到这个问题在2016版本中被解决了,但对于那些不使用该版本的人,这里提供两个通用且简化的方法。

XML 方法较为简短,但需要字符串允许进行 XML 技巧(即无“坏”字符)。

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提供的补充) 创建函数dbo.splitString(@input nVarchar(max),@Splitter nVarchar(99))返回表作为 返回值 选择Split.a.value('。','NVARCHAR(max)')作为数据来源于 (选择CAST('<M>' + REPLACE(@input,@Splitter,'</M><M>')+ '</M>' AS XML)AS Data )作为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”; 从TEST_X x cross apply dbo.splitString(x.CSV,',')Y中选择A,数据; 删除表TEST_X - Eske Rahn
这正是我所需要的!谢谢。 - Nitin Badole

10
请参考以下 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); 

结果:

颜色

红色 蓝色 绿色 黄色 黑色


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

1
这是为数不多的几种方法之一,可以在Azure SQL数据仓库中使用有限的SQL支持。 - Aaron Schultz

6

我知道有很多答案,但我想写出自己的版本分割函数,就像其他人和 string_split SQL Server 2016本地函数一样。

create function [dbo].[Split]
(
    @Value nvarchar(max),
    @Delimiter nvarchar(50)
)
returns @tbl table
(
    Seq int primary key identity(1, 1),
    Value nvarchar(max)
)
as begin
    declare @Xml xml = cast('<d>' + replace(@Value, @Delimiter, '</d><d>') + '</d>' as xml);

    insert into @tbl
            (Value)
    select  a.split.value('.', 'nvarchar(max)') as Value
    from    @Xml.nodes('/d') a(split);
    
    return;
end;
  • Seq列是主键,用于支持与其他真实表或分离函数返回的表的快速连接。
  • 使用XML函数以支持大数据(循环版本在处理大数据时会显着减慢)

这是一个问题的答案。

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


select  t.SomeID, t.OtherID, s.Value
from    Testdata t
        cross apply dbo.Split(t.String, ',') s;

--Output
SomeID  OtherID Value
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

加入Split和其他分割符
declare @Names nvarchar(max) = 'a,b,c,d';
declare @Codes nvarchar(max) = '10,20,30,40';

select  n.Seq, n.Value Name, c.Value Code
from    dbo.Split(@Names, ',') n
        inner join dbo.Split(@Codes, ',') c on n.Seq = c.Seq;

--Output
Seq Name    Code
1   a       10
2   b       20
3   c       30
4   d       40

分别拆分两次

declare @NationLocSex nvarchar(max) = 'Korea,Seoul,1;Vietnam,Kiengiang,0;China,Xian,0';

with rows as
(
    select  Value
    from    dbo.Split(@NationLocSex, ';')
)
select  rw.Value r, cl.Value c
from    rows rw
        cross apply dbo.Split(rw.Value, ',') cl;

--Output
r                       c
Korea,Seoul,1           Korea
Korea,Seoul,1           Seoul
Korea,Seoul,1           1
Vietnam,Kiengiang,0     Vietnam
Vietnam,Kiengiang,0     Kiengiang
Vietnam,Kiengiang,0     0
China,Xian,0            China
China,Xian,0            Xian
China,Xian,0            0

分割成列

declare @Numbers nvarchar(50) = 'First,Second,Third';

with t as
(
    select  case when Seq = 1 then Value end f1,
            case when Seq = 2 then Value end f2,
            case when Seq = 3 then Value end f3
    from    dbo.Split(@Numbers, ',')
)
select  min(f1) f1, min(f2) f2, min(f3) f3
from    t;

--Output
f1      f2      f3
First   Second  Third

生成一系列行数据

declare @Ranges nvarchar(50) = '1-2,4-6';

declare @Numbers table (Num int);
insert into @Numbers values (1),(2),(3),(4),(5),(6),(7),(8);

with t as
(
    select  r.Seq, r.Value,
            min(case when ft.Seq = 1 then ft.Value end) ValueFrom,
            min(case when ft.Seq = 2 then ft.Value end) ValueTo
    from    dbo.Split(@Ranges, ',') r
            cross apply dbo.Split(r.Value, '-') ft
    group by r.Seq, r.Value
)
select  t.Seq, t.Value, t.ValueFrom, t.ValueTo, n.Num
from    t
        inner join @Numbers n on n.Num between t.ValueFrom and t.ValueTo;

--Output
Seq Value   ValueFrom   ValueTo Num
1   1-2     1           2       1
1   1-2     1           2       2
2   4-6     4           6       4
2   4-6     4           6       5
2   4-6     4           6       6


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接