T-SQL:与字符串连接相反-如何将字符串拆分为多条记录[重复]

Posted

技术标签:

【中文标题】T-SQL:与字符串连接相反-如何将字符串拆分为多条记录[重复]【英文标题】:T-SQL: Opposite to string concatenation - how to split string into multiple records [duplicate] 【发布时间】:2010-09-23 19:26:50 【问题描述】:

可能重复:Split string in SQL

我在 SQL 中见过a couple of questions related to string concatenation。 我想知道您将如何处理相反的问题:将逗号分隔的字符串拆分为数据行:

假设我有桌子:

userTypedTags(userID,commaSeparatedTags) 'one entry per user
tags(tagID,name)

想往表中插入数据

userTag(userID,tagID) 'multiple entries per user

灵感来自Which tags are not in the database?问题

编辑

感谢您的回答,实际上应该接受的不止一个,但我只能选择一个,而带有递归的solution presented by Cade Roux 对我来说似乎很干净。它适用于 SQL Server 2005 及更高版本。

对于早期版本的 SQL Server,可以使用解决方案 provided by miies。 对于使用文本数据类型 wcm answer 将很有帮助。再次感谢。

【问题讨论】:

只需使用拆分例程即可。很多人已经在 SO 和其他地方发布了它的代码。 听起来你需要将该列分离到它自己的表中。如果标签只存储在一个分隔列表中,你将如何编写高效的 sql 来查找与特定标签关联的记录? Kevin,你能提供一些链接吗? 好吧,这样就更有意义了。所以我应该追捕你的前任,因为他首先制造了这个烂摊子;) 参见***.com/questions/2647/split-string-in-sql。 【参考方案1】:

您也可以使用 XML,as seen here 来实现此效果,这消除了所提供答案的限制,这些答案似乎都以某种方式包含递归。我在这里所做的特殊用途允许使用最多 32 个字符的分隔符,但可以根据需要增加多少。

create FUNCTION [dbo].[Split] (@sep VARCHAR(32), @s VARCHAR(MAX))
RETURNS TABLE
AS
    RETURN
    (
        SELECT r.value('.','VARCHAR(MAX)') as Item
        FROM (SELECT CONVERT(XML, N'<root><r>' + REPLACE(REPLACE(REPLACE(@s,'& ','&amp; '),'<','&lt;'), @sep, '</r><r>') + '</r></root>') as valxml) x
        CROSS APPLY x.valxml.nodes('//root/r') AS RECORDS(r)
    )

然后您可以使用以下方式调用它:

SELECT * FROM dbo.Split(' ', 'I hate bunnies')

返回:

-----------
|I        |
|---------|
|hate     |
|---------|
|bunnies  |
-----------


我应该注意,我实际上并不讨厌兔子……它只是出于某种原因突然出现在我的脑海中。
以下是我在内联表值函数中使用相同方法所能想到的最接近的东西。不要使用它,它非常低效!在这里仅供参考。
CREATE FUNCTION [dbo].[Split] (@sep VARCHAR(32), @s VARCHAR(MAX))
RETURNS TABLE
AS
    RETURN
    (
        SELECT r.value('.','VARCHAR(MAX)') as Item
        FROM (SELECT CONVERT(XML, N'<root><r>' + REPLACE(@s, @sep, '</r><r>') + '</r></root>') as valxml) x
        CROSS APPLY x.valxml.nodes('//root/r') AS RECORDS(r)
    )

【讨论】:

@md5sum:很好的解决方案! @md5sum - 如果你能把它变成一个内联表值函数,我会加入的。通常,标量函数在 SQL Server 上执行得非常糟糕。我很乐意看到这个以 ITVF 解决方案为基准。 7 号编辑将第一个函数更改为内联函数。这令人困惑,因为答案讨论了两种解决方案之间的差异。以防万一其他人感到困惑并看到此评论 @JoshBerke,我已编辑此答案以解决您提到的问题。 没关系,我的编辑被拒绝了,尽管我编辑的目的是恢复作者所传达的原始意图。在我看来差评。【参考方案2】:

我对“Nathan Wheeler”的答案投了赞成票,因为我发现“Cade Roux”的答案在一定的字符串大小上不起作用。

几个点

-我发现添加 DISTINCT 关键字提高了我的性能。

-Nathan 的答案仅在您的标识符为 5 个字符或更少时才有效,当然您可以调整它...如果您要拆分的项目是 INT 标识符,就像我一样,您可以和我们一样我在下面:

CREATE FUNCTION [dbo].Split
(
    @sep VARCHAR(32), 
    @s VARCHAR(MAX)
)
RETURNS 
    @result TABLE (
        Id INT NULL
    )   
AS
BEGIN
    DECLARE @xml XML
    SET @XML = N'<root><r>' + REPLACE(@s, @sep, '</r><r>') + '</r></root>'

    INSERT INTO @result(Id)
    SELECT DISTINCT r.value('.','int') as Item
    FROM @xml.nodes('//root//r') AS RECORDS(r)

    RETURN
END

【讨论】:

根据拆分的内容,使用DISTINCT 可能会产生负面影响。也就是说,结果表可能应该包含一些重复的值,但对于DISTINCT,它只有唯一值。【参考方案3】:

我通常用下面的代码来做这个:

create function [dbo].[Split](@string varchar(max), @separator varchar(10))
returns @splited table ( stringPart varchar(max) )
with execute as caller
as
begin
    declare @stringPart varchar(max);
    set @stringPart = '';

    while charindex(@separator, @string) > 0
    begin
        set @stringPart = substring(@string, 0, charindex(@separator, @string));
        insert into @splited (stringPart) values (@stringPart);
        set @string = substring(@string, charindex(@separator, @string) + len(@separator), len(@string) + 1);
    end

    return;
end
go

您可以使用以下查询对其进行测试:

declare @example varchar(max);
set @example = 'one;string;to;rule;them;all;;';

select * from [dbo].[Split](@example, ';');

【讨论】:

【参考方案4】:

使用 CLR,这是一个更简单的替代方案,适用于所有情况,但比公认的答案快 40%:

using System;
using System.Collections;
using System.Data.SqlTypes;
using System.Text.RegularExpressions;
using Microsoft.SqlServer.Server;

public class UDF

    [SqlFunction(FillRowMethodName="FillRow")]
    public static IEnumerable RegexSplit(SqlString s, SqlString delimiter)
    
        return Regex.Split(s.Value, delimiter.Value);
    

    public static void FillRow(object row, out SqlString str)
    
        str = new SqlString((string) row);
    

当然,它仍然比 PostgreSQL 的 regexp_split_to_table 慢 8 倍。

【讨论】:

EXPLAIN ANALYZE with PostgreSQL,以及它的穷人版本,即用 SQL Server 检查 SSMS 中的“包括实际执行计划”。两个数据库中包含数百万条记录的完全相同的表。 我喜欢这个(虽然我还没有对它进行基准测试),但是您应该在 SqlFunction 属性中包含 TableDefinition 属性,以便数据工具可以生成正确的函数定义。【参考方案5】:

这是一个与 2005 年之前的 SQL Server 版本兼容的 Split 函数。

CREATE FUNCTION dbo.Split(@data nvarchar(4000), @delimiter nvarchar(100))  
RETURNS @result table (Id int identity(1,1), Data nvarchar(4000)) 
AS  
BEGIN 
    DECLARE @pos   INT
    DECLARE @start INT
    DECLARE @len   INT
    DECLARE @end   INT

    SET @len   = LEN('.' + @delimiter + '.') - 2
    SET @end   = LEN(@data) + 1
    SET @start = 1
    SET @pos   = 0

    WHILE (@pos < @end)
    BEGIN
        SET @pos = CHARINDEX(@delimiter, @data, @start)
        IF (@pos = 0) SET @pos = @end

        INSERT @result (data) SELECT SUBSTRING(@data, @start, @pos - @start)
        SET @start = @pos + @len
    END

    RETURN
END

【讨论】:

+1 用于避免递归(因为 SQL Server 做得很差),避免 XML(因为 SQL 没有用于转义特殊 XML 字符的简单 API),还避免 CLR 代码(因为一些公司' 数据中心不允许在共享 SQL Server 实例上使用自定义代码)。【参考方案6】:

我使用此功能(SQL Server 2005 及更高版本)。

create function [dbo].[Split]
(
    @string nvarchar(4000),
    @delimiter nvarchar(10)
)
returns @table table
(
    [Value] nvarchar(4000)
)
begin
    declare @nextString nvarchar(4000)
    declare @pos int, @nextPos int

    set @nextString = ''
    set @string = @string + @delimiter

    set @pos = charindex(@delimiter, @string)
    set @nextPos = 1
    while (@pos <> 0)
    begin
        set @nextString = substring(@string, 1, @pos - 1)

        insert into @table
        (
            [Value]
        )
        values
        (
            @nextString
        )

        set @string = substring(@string, @pos + len(@delimiter), len(@string))
        set @nextPos = @pos
        set @pos = charindex(@delimiter, @string)
    end
    return
end

【讨论】:

谢谢。我认为这也适用于 SQLServer 2000 你是对的。我以为 SQL Server 2005 引入了表值函数,但事实并非如此。 @commaCheck 没有被使用,除了一个赋值,所以它可以被移除。 另外,设置@string = substring(@string, @pos + 1, len(@string)) 应该设置@string = substring(@string, @pos + len(@delimiter), len(@string))【参考方案7】:

对于将字符串拆分为单词的特殊情况,我遇到了另一种 SQL Server 2008 解决方案。

with testTable AS
(
SELECT 1 AS Id, N'how now brown cow' AS txt UNION ALL
SELECT 2, N'she sells sea shells upon the sea shore' UNION ALL
SELECT 3, N'red lorry yellow lorry' UNION ALL
SELECT 4, N'the quick brown fox jumped over the lazy dog'
)

SELECT display_term, COUNT(*) As Cnt
 FROM testTable
CROSS APPLY sys.dm_fts_parser('"' + txt + '"', 1033, 0,0)
GROUP BY display_term
HAVING COUNT(*) > 1
ORDER BY Cnt DESC

返回

display_term                   Cnt
------------------------------ -----------
the                            3
brown                          2
lorry                          2
sea                            2

【讨论】:

有趣,但必须注意它需要“全文搜索”才能安装并可用 @quetzalcoatl - 它还需要sysadmin 权限。仍然可能对某人有用。 绝对完美。这就像“Stuff”命令的反面。无需测试大量拆分功能,只是怀疑您是否真的想在生产环境中安装它。非常适合我的要求。谢谢!【参考方案8】:

对上面的the solution 稍作修改,使其适用于可变长度分隔符。

create FUNCTION dbo.fn_Split2 (@sep nvarchar(10), @s nvarchar(4000))
RETURNS table
AS
RETURN (
    WITH Pieces(pn, start, stop) AS (
      SELECT 1, 1, CHARINDEX(@sep, @s)
      UNION ALL
      SELECT pn + 1, stop + (datalength(@sep)/2), CHARINDEX(@sep, @s, stop + (datalength(@sep)/2))
      FROM Pieces
      WHERE stop > 0
    )
    SELECT pn,
      SUBSTRING(@s, start, CASE WHEN stop > 0 THEN stop-start ELSE 4000 END) AS s
    FROM Pieces
  )

注意:我使用了 datalength(),因为如果有尾随空格,len() 会错误地报告。

【讨论】:

【参考方案9】:

documented here 这个问题有多种解决方案,包括这个小宝石:

CREATE FUNCTION dbo.Split (@sep char(1), @s varchar(512))
RETURNS table
AS
RETURN (
    WITH Pieces(pn, start, stop) AS (
      SELECT 1, 1, CHARINDEX(@sep, @s)
      UNION ALL
      SELECT pn + 1, stop + 1, CHARINDEX(@sep, @s, stop + 1)
      FROM Pieces
      WHERE stop > 0
    )
    SELECT pn,
      SUBSTRING(@s, start, CASE WHEN stop > 0 THEN stop-start ELSE 512 END) AS s
    FROM Pieces
  )

【讨论】:

很棒的功能。可以使用 nchar() 和 nvarchar()。另请参阅下面有关可变长度分隔符的建议。 在 SQL Server 2008 上,当列表中有超过 101 项时,此版本失败:“语句终止。在语句完成之前,最大递归 100 已用完。” @MikeSchenk 您可以使用 OPTION (MAXRECURSION n) 提示 (msdn.microsoft.com/en-us/library/ms181714.aspx) 更改递归级别 - 但是,UDF 定义中不允许这样做。这个问题 (social.msdn.microsoft.com/forums/en-US/transactsql/thread/…) 暗示您可以在 UDF 之外指定它并让它仍然有效。 警告:输入字符串较大(超过约 1000 个字符)失败。 "在语句完成之前,最大递归 100 已经用完。" 如何从选择查询中调用此函数?我收到以下错误:找不到列“dbo”或用户定义的函数或聚合“dbo.fn_Split”,或者名称不明确。【参考方案10】:
SELECT substring(commaSeparatedTags,0,charindex(',',commaSeparatedTags))

会给你第一个标签。您可以通过每次将 substring 和 charindex 组合更深一层来进行类似的操作以获取第二个,依此类推。这是一个立竿见影的解决方案,但它只适用于很少的标签,因为查询的大小增长得非常快并且变得不可读。然后继续讨论函数,如本文其他更复杂的答案所述。

【讨论】:

【参考方案11】:

这是我不久前写的。它假定分隔符是逗号,并且各个值不大于 127 个字符。它可以很容易地修改。

它的好处是不限于 4,000 个字符。

祝你好运!

ALTER Function [dbo].[SplitStr] ( 
        @txt text 
) 
Returns @tmp Table 
        ( 
                value varchar(127)
        ) 
as 
BEGIN 
        declare @str varchar(8000) 
                , @Beg int 
                , @last int 
                , @size int 

        set @size=datalength(@txt) 
        set @Beg=1 


        set @str=substring(@txt,@Beg,8000) 
        IF len(@str)<8000 set @Beg=@size 
        ELSE BEGIN 
                set @last=charindex(',', reverse(@str)) 
                set @str=substring(@txt,@Beg,8000-@last) 
                set @Beg=@Beg+8000-@last+1 
        END 

        declare @workingString varchar(25) 
                , @stringindex int 



        while @Beg<=@size Begin 
                WHILE LEN(@str) > 0 BEGIN 
                        SELECT @StringIndex = CHARINDEX(',', @str) 

                        SELECT 
                                @workingString = CASE 
                                        WHEN @StringIndex > 0 THEN SUBSTRING(@str, 1, @StringIndex-1) 
                                        ELSE @str 
                                END 

                        INSERT INTO 
                                @tmp(value)
                        VALUES 
                                (cast(rtrim(ltrim(@workingString)) as varchar(127)))
                        SELECT @str = CASE 
                                WHEN CHARINDEX(',', @str) > 0 THEN SUBSTRING(@str, @StringIndex+1, LEN(@str)) 
                                ELSE '' 
                        END 
                END 
                set @str=substring(@txt,@Beg,8000) 

                if @Beg=@size set @Beg=@Beg+1 
                else IF len(@str)<8000 set @Beg=@size 
                ELSE BEGIN 
                        set @last=charindex(',', reverse(@str)) 
                        set @str=substring(@txt,@Beg,8000-@last) 
                        set @Beg=@Beg+8000-@last+1 

                END 
        END     

        return
END 

【讨论】:

很棒+不受限制!!谢谢

以上是关于T-SQL:与字符串连接相反-如何将字符串拆分为多条记录[重复]的主要内容,如果未能解决你的问题,请参考以下文章

将 T-SQL 表值函数字符串拆分为 C#

T-SQL基于分隔符拆分列并将拆分后的字符串数组输入到多个表列中

将字符串拆分为单词并与其他数据重新连接

T-SQL拆分使用指定分隔符的字符串(split string)

动态规划之字符串拆分

SQL Server 2008 T-SQL UDF Split() 剪裁