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

Turning a Comma Separated string into individual rows

提问人:Michael Stum 提问时间:3/31/2011 最后编辑:AnonymousMichael Stum 更新时间:6/19/2023 访问量:706645

问:

我有一个这样的SQL表:

SomeID 其他 ID 数据
abcdef-..... cdef123-... 18,20,22
abcdef-..... 4554a24-... 17,19
987654-..... 12324一2-... 13,19,20

是否有查询,我可以执行这样的查询,返回单个行,如下所示:SELECT OtherID, SplitData WHERE SomeID = 'abcdef-.......'

其他 ID 拆分数据
cdef123-... 18
cdef123-... 20
cdef123-... 22
4554a24-... 17
4554a24-... 19

基本上将逗号处的数据拆分为单独的行?

我知道将字符串存储到关系数据库中听起来很愚蠢,但消费者应用程序中的正常用例确实很有帮助。comma-separated

我不想在应用程序中进行拆分,因为我需要分页,所以我想在重构整个应用程序之前探索选项。

它是(非 R2)。SQL Server 2008

sql-server csv t-sql 拆分

评论

0赞 Rick James 7/5/2019
Смотритетакже: periscopedata.com/blog/...

答:

319赞 RichardTheKiwi 3/31/2011 #1

您可以使用 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赞 ca9163d9 2/22/2012
如果将列的数据类型从 更改为 ,则代码不起作用,例如?Datavarchar(max)varchar(4000)create table Testdata(SomeID int, OtherID int, Data varchar(4000))
4赞 RichardTheKiwi 2/22/2012
@NickW这可能是因为 UNION ALL 之前和之后的部分从 LEFT 函数返回不同的类型。就我个人而言,我不明白为什么一旦你达到 4000 美元,你就不会跳到 MAX......
0赞 dsz 1/14/2014
对于一组 BIG 值,这可能会超出 CTE 的递归限制。
3赞 RichardTheKiwi 1/15/2014
@dsz 那是你使用OPTION (maxrecursion 0)
18赞 smoore4 7/15/2016
LEFT 函数可能需要 CAST 才能工作。例如,LEFT(CAST(Data AS VARCHAR(MAX))....
3赞 Klix Media 7/28/2012 #2
;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 7/29/2012
您能否简要解释一下这是对已接受答案中的版本的改进?
0赞 TamusJRoyce 1/6/2015
没有工会......更少的代码。既然它使用的是 union all 而不是 union,那么不应该是性能差异吗?
1赞 Oedhel Setren 2/27/2015
这没有返回它应该包含的所有行。我不确定数据需要联合全部,但您的解决方案返回的行数与原始表相同。
1赞 Eske Rahn 4/8/2016
(这里的问题是递归部分是省略的那个......
0赞 Ankit Misra 7/9/2019
没有给我预期的输出,只在单独的行中给出第一条记录
102赞 bvr 4/18/2013 #3

检查这个

 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赞 user1151923 3/5/2015
使用此方法时,必须确保所有值都不包含非法 XML 的内容
0赞 Control 7/9/2015
这太棒了。我能问你,如果我希望新列只显示拆分字符串中的第一个字符,我将如何重写它?
1赞 Jeff Moden 6/12/2020
我必须告诉你,这种方法被称为“XML Splitter Method”,它几乎和 While 循环或递归 CTE 一样慢。我强烈建议您始终避免使用它。请改用 DelimitedSplit8K。除了 2016 年的 Split_String() 函数或编写良好的 CLR 之外,它打破了一切。
0赞 Eduo 1/3/2023
@JeffModen 然而,这样做的优点是可以在不允许/不可能创建函数(甚至是临时函数)的环境中运行。
0赞 Jeff Moden 1/4/2023
@Eduo - 完美。在每个环境中,它都可能相对较慢。;)
6赞 Jayvee 11/7/2013 #4
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赞 Aaron Schultz 10/17/2017
这是适用于 Azure SQL 数据仓库中有限 SQL 支持的少数方法之一。
15赞 dsz 1/14/2014 #5

截至 2016 年 2 月 - 参见 TALLY 表示例 - 从 2014 年 2 月开始,很可能会超过下面的 TVF。保留以下原始帖子供后代使用:


在上面的例子中,我喜欢的重复代码太多了。而且我不喜欢 CTE 和 XML 的性能。此外,显式,以便特定于订单的使用者可以指定子句。IdORDER 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;
23赞 tomlost 1/28/2014 #6
select t.OtherID,x.Kod
    from testData t
    cross apply (select Code from dbo.Split(t.Data,',') ) x

评论

3赞 tobriand 11/3/2015
完全符合我所追求的,并且比许多其他示例更容易阅读(前提是数据库中已经有一个用于分隔字符串拆分的函数)。作为以前不熟悉的人,这有点有用!CROSS APPLY
0赞 Jayendran 6/7/2017
我无法理解这部分(从 dbo 中选择代码。Split(t.Data,',') ) ?dbo。拆分是一个表,它在哪里存在,代码是拆分表中的列?我在此页面的任何地方都找不到这些表或值的列表?
1赞 Akbar Kautsar 8/11/2017
我的工作代码是:select t.OtherID, x.* from testData t cross apply (select item as Data from dbo.Split(t.Data,',') ) x
2赞 tommylux 3/17/2015 #7

使用此方法时,必须确保任何值都不包含非法 XML – user1151923

我总是使用 XML 方法。请确保使用有效的 XML。我有两个函数可以在有效的 XML 和 Text 之间进行转换。(我倾向于去掉回车,因为我通常不需要它们。

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赞 Stewart 5/1/2017
你那里的代码有一个小问题。它会将“<”更改为“&lt;' 而不是 '<',就像它应该的那样。因此,您需要先对“&”进行编码。
0赞 Shnugo 7/19/2018
不需要这样的功能......只需使用隐性能力即可。试试这个:SELECT (SELECT '<&> blah' + CHAR(13)+CHAR(10) + 'next line' FOR XML PATH(''))
235赞 Pரதீப் 3/30/2016 #8

最后,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

对于旧版本,这里使用 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 拆分器”函数

评论

0赞 Stewart 4/29/2017
如果只有服务器在 SQL Server 2016 上,我会使用 STRING_SPLIT!顺便说一句,根据您链接到的页面,它输出的字段名称是 ,而不是 。valueSplitData
7赞 Mystus 1/13/2021
公认的答案是有效的,但考虑到现在是 2021 年,这是现在应该优先考虑的答案。谢谢你 - SPLIT_STRING这正是我想要的。
0赞 krystof236 3/30/2022
如果原始数据包含要按STRING_SPLIT拆分的列(本问题中的“Data”列)中具有 NULL 值的行,则在使用 CROSS APPLY 时,结果中将省略这些行(在本问题中为“SplitData”列)。要保留这些内容,请使用 OUTER APPLY。
0赞 Geoff 1/27/2023
需要兼容级别 130 以及 SQL Server 2016
11赞 Eske Rahn 4/2/2016 #9

很高兴看到它在 2016 版本中得到了解决,但对于所有没有解决的问题,这里有上述方法的两个通用和简化版本。

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 方法 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赞 jpaugh 9/1/2016
这似乎是显而易见的,但是如何使用这两个功能呢?特别是,您能展示如何在 OP 的用例中使用它吗?
1赞 Eske Rahn 9/9/2016
下面是一个简单示例:创建表 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;删除表TEST_X
0赞 Nitin Badole 1/18/2019
这正是我需要的!谢谢。
0赞 Arun Pratap Singh 10/6/2017 #10

下面适用于 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 

将获得所有笛卡尔积,其原点表列加上拆分表的“项目”。

2赞 mr R 1/31/2018 #11

功能

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;

评论

2赞 Sean Lange 3/13/2019
在多语句表值函数中使用 while 循环几乎是拆分字符串的最糟糕方法。关于这个问题,已经有很多基于集合的选项。
10赞 Jag Kandasamy 8/22/2018 #12

请参考下面的 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); 

结果:

颜色

红 蓝 绿 黄色 黑

35赞 Dungeon 1/7/2019 #13

很晚了,但试试这个:

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赞 Craig Silver 6/26/2019
STRING_SPLIT很漂亮,但它需要 SQL Server 2016。learn.microsoft.com/en-us/sql/t-sql/functions/......
2赞 Sangram Nandkhile 5/28/2020
优雅的解决方案。
2赞 N Khan 10/7/2020
是的,@SangramNandkhile真的说了,这是最优雅的解决方案,不需要声明任何变量,注释好的代码,这就是我想要的。感谢 Dungeon
1赞 Jamie 7/3/2022
这里有很多好的答案,但这个答案很简短。它还允许仅使用row_number作为排序方法从数组中提取特定项目,并使用模数仅从字段中列出的数组中提取一条记录。也省去了重写拆分函数的麻烦。
0赞 Dermo909 12/1/2022
很棒的东西
0赞 Spider 2/13/2019 #14

您可以使用以下函数提取数据

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;

评论

1赞 Sean Lange 3/13/2019
在多语句表值函数中使用 while 循环几乎是拆分字符串的最糟糕方法。关于这个问题,已经有很多基于集合的选项。
3赞 Hugues Gauthier 8/11/2020 #15

通过创建这个拆分字符串的函数 ([DelimitedSplit]),您可以对 SELECT 执行 OUTER APPLY。

CREATE FUNCTION [dbo].[DelimitedSplit]
--===== Define I/O parameters
        (@pString VARCHAR(8000), @pDelimiter CHAR(1))
--WARNING!!! DO NOT USE MAX DATA-TYPES HERE!  IT WILL KILL PERFORMANCE!
RETURNS TABLE WITH SCHEMABINDING AS
 RETURN
--===== "Inline" CTE Driven "Tally Table" produces values from 1 up to 10,000...
     -- enough to cover VARCHAR(8000)
  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 INNER JOIN E1 b ON b.N = a.N), --10E+2 or 100 rows
       E4(N) AS (SELECT 1 FROM E2 a INNER JOIN E2 b ON b.N = a.N), --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
;

测试

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
 *
FROM #Testdata
OUTER APPLY [dbo].[DelimitedSplit](String,',');

DROP TABLE #Testdata;

结果

SomeID  OtherID String      ItemNumber  Item
1       9       18,20,22    1           18
1       9       18,20,22    2           20
1       9       18,20,22    3           22
2       8       17,19       1           17
2       8       17,19       2           19
3       7       13,19,20    1           13
3       7       13,19,20    2           19
3       7       13,19,20    3           20
4       6       1   
9       11      1,2,3,4     1           1
9       11      1,2,3,4     2           2
9       11      1,2,3,4     3           3
9       11      1,2,3,4     4           4
6赞 doctorgu 8/13/2020 #16

我知道它有很多答案,但我想像其他人一样编写我的拆分函数版本,就像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 列是主键,支持与其他实表或 Split 函数返回表的快速连接。
  • 使用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

0赞 lemon 6/1/2023 #17

从 SQL Server 2016 (13.x)SQL Server 2016 (13.x)SQL Server 2016 (13.x)SQL Server 2016 (13.x)SQL Server 2016 (13.x)SQL Server 2016 (13.x)SQL Server 2016 (13.x)SQL Server 2016 (13.x

SELECT tab.SomeID, tab.OtherID, value AS val
FROM tab
CROSS APPLY OPENJSON(CONCAT('[', tab.Data, ']'))

输出

SomeID 其他 ID 瓦尔
abcdef-..... cdef123-... 18
abcdef-..... cdef123-... 20
abcdef-..... cdef123-... 22
abcdef-..... 4554a24-... 17
abcdef-..... 4554a24-... 19
987654-..... 12324一2-... 13
987654-..... 12324一2-... 19
987654-..... 12324一2-... 20

在此处查看演示。

2赞 Hesham Yemen 6/19/2023 #18

以下是如何使用STRING_SPLIT的示例

DECLARE @MY_VALUES NVARCHAR(100)

SET @MY_VALUES = 'Apple,Orange,Banana,Coconut'

SELECT VALUE FROM STRING_SPLIT(@MY_VALUES, ',');

结果:

Apple
Orange
Banana
Coconut