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

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个回答

3
;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
没有union all...更少的代码。由于它使用的是union all而不是union,所以不应该有性能差异吗? - TamusJRoyce
1
这个查询没有返回所有应该返回的行。我不确定数据中需要什么样的联结,但是你的解决方案返回了与原始表格相同数量的行。 - Oedhel Setren
1
(这里的问题在于递归部分被省略了...) - Eske Rahn
没有给我期望的输出,只是单独在一行中给出第一条记录。 - Ankit Misra

3
通过创建这个函数([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

2

使用这种方法时,您必须确保您的值中没有包含任何非法的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;”,而不是应该的“<”。因此,你需要先对“&”进行编码。 - Stewart
不需要这样的函数...只需使用隐式功能。试试这个:SELECT (SELECT '<&> blah' + CHAR(13)+CHAR(10) + 'next line' FOR XML PATH('')) - Shnugo

2
功能
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
在多语句表值函数中使用 while 循环来拆分字符串几乎是最糟糕的方式。这个问题已经有很多基于集合的选项了。 - Sean Lange

2
这是一个使用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

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 

将获取原始表列与拆分表的“items”一起的所有笛卡尔积。


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;

1
在多语句表值函数中使用while循环来拆分字符串几乎是最糟糕的方式。这个问题已经有很多基于集合的选项了。 - Sean Lange

0

从 SQL Server 2016 (13.x) 开始,可以使用 OPENJSON 来完成此任务,具体如下:

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

输出:

SomeID OtherID val
abcdef-..... cdef123-... 18
abcdef-..... cdef123-... 20
abcdef-..... cdef123-... 22
abcdef-..... 4554a24-... 17
abcdef-..... 4554a24-... 19
987654-..... 12324a2-... 13
987654-..... 12324a2-... 19
987654-..... 12324a2-... 20

请在这里检查演示。


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