使用 T-SQL,从字符串中返回第 n 个分隔元素

Posted

技术标签:

【中文标题】使用 T-SQL,从字符串中返回第 n 个分隔元素【英文标题】:Using T-SQL, return nth delimited element from a string 【发布时间】:2013-10-27 06:53:37 【问题描述】:

我需要创建一个函数,它将返回分隔字符串的第 n 个元素。

对于一个数据迁移项目,我使用 SQL 脚本将存储在 SQL Server 数据库中的 JSON 审计记录转换为结构化报告。目标是在没有任何代码的情况下交付一个 sql 脚本和脚本使用的 sql 函数。

(这是一个短期修复,将在 ASP.NET/MVC 应用程序中添加新的审核功能时使用)

不乏可用的分隔字符串到表格示例。 我选择了一个公共表表达式示例http://www.sqlperformance.com/2012/07/t-sql-queries/split-strings

示例:我想从 '1,222,2,67,888,1111' 返回 67

【问题讨论】:

【参考方案1】:

您可以将 STRING_SPLITROW_NUMBER 一起使用:

SELECT value, idx FROM
(
  SELECT
    value,
    ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) idx
  FROM STRING_SPLIT('Lorem ipsum dolor sit amet.', ' ')
) t
WHERE idx=2

返回第二个元素 (idx=2):'ipsum'

【讨论】:

fine manual 状态 The output rows might be in any order. The order is not guaranteed to match the order of the substrings in the input string. 【参考方案2】:

在一个罕见的疯狂时刻,我只是认为如果我们使用 XML 为我们解析它,拆分会容易得多:

(使用@Gary Kindel 答案中的变量)

declare @xml xml
set @xml = '<split><el>' + replace(@list,@Delimiter,'</el><el>') + '</el></split>'

select
    el = split.el.value('.','varchar(max)')
from  @xml.nodes('/split/el') split(el))

这列出了字符串的所有元素,按指定字符分割。

我们可以使用 xpath 测试过滤掉空值,并使用进一步的 xpath 测试将其限制为我们感兴趣的元素。完整的 Gary 函数变为:

alter FUNCTION dbo.GetSplitString_CTE
(
   @List       VARCHAR(MAX),
   @Delimiter  VARCHAR(255),
   @ElementNumber int
)
RETURNS VARCHAR(max)
AS
BEGIN
       -- escape any XML https://dba.stackexchange.com/a/143140/65992  
       set @list = convert(VARCHAR(MAX),(select @list for xml path(''), type));

       declare @xml xml
       set @xml = '<split><el>' + replace(@list,@Delimiter,'</el><el>') + '</el></split>'

       declare @ret varchar(max)
       set @ret = (select
              el = split.el.value('.','varchar(max)')
       from  @xml.nodes('/split/el[string-length(.)>0][position() = sql:variable("@elementnumber")]') split(el))

       return @ret

END

【讨论】:

很好的解决方案。我想知道带有身份或 XML 的表变量更昂贵。乔恩,我必须创建你的函数并在大型结果集上与 CTE 解决方案并排运行,看看哪个使用更多资源。 有兴趣知道 - CTE 做了很多字符串操作。对于您的示例(仅在元素中使用数字),我没有费心解析非法内容并替换为 xml 实体(例如' =&gt; @apos;)。对于更复杂的字符串,您可能需要(但拆分操作通常不是这种情况) 只需重新阅读您的评论。如果您想存储这些内容,您仍然可以轻松地将输出保存到表变量中。比较的是两种解析机制。 @JonEgerton 这可以更容易地完成......没有必要先用.nodes() 创建一个列表,然后选择正确的元素。您可以直接使用该位置。我自己放了一个答案,你可能会感兴趣...... 我添加了一些 XML 转义,以防 @list 中有任何 XML 字符,例如 & 在其中【参考方案3】:

这是恢复 67 的最简单答案(type-safe!!):

SELECT CAST('<x>' + REPLACE('1,222,2,67,888,1111',',','</x><x>') + '</x>' AS XML).value('/x[4]','int')

在下文中,您将找到示例如何将其与字符串、分隔符和位置的变量一起使用(即使对于带有 XML 禁止字符的边缘情况)

简单的

这个问题不是关于字符串拆分方法,而是关于如何获取第 n 个元素。最简单、完全可内联的方式就是这个 IMO:

这是一个真正的单行,以空格分隔第 2 部分:

DECLARE @input NVARCHAR(100)=N'part1 part2 part3';
SELECT CAST(N'<x>' + REPLACE(@input,N' ',N'</x><x>') + N'</x>' AS XML).value('/x[2]','nvarchar(max)')

变量可以与sql:variable()sql:column() 一起使用

当然您可以使用变量作为分隔符和位置(使用sql:column 直接从查询值中检索位置):

DECLARE @dlmt NVARCHAR(10)=N' ';
DECLARE @pos INT = 2;
SELECT CAST(N'<x>' + REPLACE(@input,@dlmt,N'</x><x>') + N'</x>' AS XML).value('/x[sql:variable("@pos")][1]','nvarchar(max)')

带有 XML 禁止字符的 Edge-Case

如果您的字符串可能包含禁止字符,您仍然可以这样做。只需先在字符串上使用FOR XML PATH,即可将所有禁用字符隐式替换为合适的转义序列。

如果 - 另外 - 您的分隔符是分号,这是一个非常特殊的情况。在这种情况下,我首先将分隔符替换为“#DLMT#”,最后将其替换为 XML 标记:

SET @input=N'Some <, > and &;Other äöü@€;One more';
SET @dlmt=N';';
SELECT CAST(N'<x>' + REPLACE((SELECT REPLACE(@input,@dlmt,'#DLMT#') AS [*] FOR XML PATH('')),N'#DLMT#',N'</x><x>') + N'</x>' AS XML).value('/x[sql:variable("@pos")][1]','nvarchar(max)');

SQL-Server 2016+ 更新

很遗憾,开发人员忘记返回带有STRING_SPLIT 的部件索引。但是,使用 SQL-Server 2016+,有 JSON_VALUEOPENJSON

使用JSON_VALUE,我们可以将位置作为索引数组传递。

对于OPENJSON,documentation 明确说明:

当 OPENJSON 解析 JSON 数组时,该函数返回 JSON 文本中元素的索引作为键。

1,2,3 这样的字符串只需要括号:[1,2,3]。 像this is an example 这样的一串单词必须是["this","is","an"," example"]。 这些是非常简单的字符串操作。试试看吧:

DECLARE @str VARCHAR(100)='Hello John Smith';
DECLARE @position INT = 2;

--We can build the json-path '$[1]' using CONCAT
SELECT JSON_VALUE('["' + REPLACE(@str,' ','","') + '"]',CONCAT('$[',@position-1,']'));

--查看这个位置安全的字符串拆分器(从零开始):

SELECT  JsonArray.[key] AS [Position]
       ,JsonArray.[value] AS [Part]
FROM OPENJSON('["' + REPLACE(@str,' ','","') + '"]') JsonArray

在this post我测试了各种方法,发现OPENJSON真的很快。甚至比著名的“delimitedSplit8k()”方法还要快...

更新 2 - 获取类型安全的值

我们可以在数组中使用数组,只需使用双倍的[[]]。这允许输入WITH-clause:

DECLARE  @SomeDelimitedString VARCHAR(100)='part1|1|20190920';

DECLARE @JsonArray NVARCHAR(MAX)=CONCAT('[["',REPLACE(@SomeDelimitedString,'|','","'),'"]]');

SELECT @SomeDelimitedString          AS TheOriginal
      ,@JsonArray                    AS TransformedToJSON
      ,ValuesFromTheArray.*
FROM OPENJSON(@JsonArray)
WITH(TheFirstFragment VARCHAR(100) '$[0]'
    ,TheSecondFragment INT '$[1]'
    ,TheThirdFragment DATE '$[2]') ValuesFromTheArray

【讨论】:

虽然这有效,但在某些字符串上我收到错误“JSON 文本格式不正确。在位置 105 发现意外字符 'F'。”知道那里发生了什么吗? 这样一个失败的字符串示例:qsdfqsdfsdf"qsdfqsdf 基本上当字符串包含引号时。 @Schoof 你可能会use STRING_ESCAPE(),它目前只支持 JSON :-) 谢谢!这可行,但不幸的是,最终结果比使用 STRING_SPLIT 慢很多。 @Schoof 您可以使用简单的 replace() 代替。当涉及到 magic charactersmarkup... 时,字符串总是很棘手【参考方案4】:

或者,可以使用xmlnodes()ROW_NUMBER。我们可以根据它们的document order 对元素进行排序。例如:

DECLARE @Input VARCHAR(100) = '1a,2b,3c,4d,5e,6f,7g,8h'
       ,@Number TINYINT = 3

DECLARE @XML XML;
DECLARE @value VARCHAR(100);

SET @XML = CAST('<x>' + REPLACE(@Input,',','</x><x>') + '</x>' AS XML);

WITH DataSource ([rowID], [rowValue]) AS
(
    SELECT ROW_NUMBER() OVER (ORDER BY T.c ASC) 
            ,T.c.value('.', 'VARCHAR(100)')
    FROM @XML.nodes('./x') T(c)
)
SELECT @value = [rowValue]
FROM DataSource
WHERE [rowID] = @Number;

SELECT @value;

【讨论】:

【参考方案5】:

我宁愿创建一个带有标识列的临时表,并用SPLIT 函数的输出填充它。

  CREATE TABLE #tblVals(Id INT IDENTITY(1,1), Val NVARCHAR(100))
  INSERT INTO #tblVals (Val)
  SELECT [value] FROM STRING_SPLIT('Val1-Val3-Val2-Val5', '-')
  SELECT * FROM #tblVals

现在您可以轻松地执行以下操作。

DECLARE @val2 NVARCHAR(100) = (SELECT TOP 1 Val FROM #tblVals WHERE Id = 2)

请看下面的快照:

【讨论】:

您无法保证 STRING_SPLIT 将按照它们在输入字符串中的相同顺序返回值。我为 STRING SPLIT 引用 the documentation:The output rows might be in any order. The order is not guaranteed to match the order of the substrings in the input string. 另外,您不需要临时表、自动 int 等,您可以只需要 SELECT[value],ROW_NUMBER()OVER() as i FROM STRING_SPLIT...,但如上所述.. 没有订单保证..【参考方案6】:

@a - 值 (f.e. 'a/bb/ccc/dddd/ee/ff/....')

@p - 想要的位置 (1,2,3...)

@d - 分隔符('/')

trim(子串(替换(@a,@d,replicate(' ',len(@a))),(@p-1)*len(@a)+1,len(@a)))

唯一的问题是 - 如果需要的部分有尾随或前导空白,它们会被修剪。

完全基于来自https://exceljet.net/formula/split-text-with-delimiter的文章

【讨论】:

【参考方案7】:

怎么样:

CREATE FUNCTION dbo.NTH_ELEMENT (@Input NVARCHAR(MAX), @Delim CHAR = '-', @N INT = 0)
RETURNS NVARCHAR(MAX)
AS
BEGIN
RETURN (SELECT VALUE FROM STRING_SPLIT(@Input, @Delim) ORDER BY (SELECT NULL) OFFSET @N ROWS FETCH NEXT 1 ROW ONLY)
END

【讨论】:

STRING_SPLIT() 不保证按给定顺序返回元素。由于这需要 v2016,因此使用 OPENJSON 是一种更好的方法,它返回一个包含 JSON 数组中元素索引的 [key]。你可以read this 感谢@Schugo - JSON 的另一个用途,我没想到! 很遗憾,您不能对 STRING_SPLIT() 的作者投反对票 - 没有订购?这几乎完全杀死了它。它应该返回 idx,value,因此您可以对索引进行排序...(或哇:从 idx=@idx 的 string_Split(str,delim) 中选择值)并在几秒钟内实现人们想要的......将是一个班轮,如果他们在实施之前花了几分钟并设计了它。惊人的。我本来打算用这个函数的,但现在我想不出它的用途,除非你碰巧有一组往往很少见的无序的东西。 所以使用@Schnugo 的建议,我提出的解决方案变成了CREATE OR ALTER FUNCTION dbo.NTH_ELEMENT (@Input NVARCHAR(MAX), @Delim CHAR = '-', @N INT = 0) RETURNS NVARCHAR(MAX) AS BEGIN RETURN (SELECT value FROM OPENJSON('["' + REPLACE(@Input, @Delim, '","') + '"]') WHERE [key] = @N) END【参考方案8】:

由于我的声誉低,我无法评论 Gary 的解决方案

我知道 Gary 引用了另一个链接。

我一直在努力理解为什么我们需要这个变量

@ld INT = LEN(@Delimiter)

我也不明白为什么 charindex 必须从分隔符长度的位置开始,@ld

我测试了许多使用单个字符分隔符的示例,它们都有效。大多数情况下,分隔符是单个字符。但是,由于开发人员将 ld 作为分隔符的长度,因此代码必须适用于具有多个字符的分隔符

在这种情况下,下面的情况会失败

11,,,22,,,33,,,44,,,55,,,

我从这个链接的代码中克隆。 http://codebetter.com/raymondlewallen/2005/10/26/quick-t-sql-to-parse-a-delimited-string/

我已经测试了各种场景,包括有多个字符的分隔符

alter FUNCTION [dbo].[split1]
(
    @string1 VARCHAR(8000) -- List of delimited items
    , @Delimiter VARCHAR(40) = ',' -- delimiter that separates items
    , @ElementNumber int
)
RETURNS varchar(8000)
AS
BEGIN
    declare @position int
    declare @piece varchar(8000)=''
    declare @returnVal varchar(8000)=''
    declare @Pattern varchar(50) = '%' + @Delimiter + '%'
    declare @counter int =0
    declare @ld int = len(@Delimiter)
    declare @ls1 int = len (@string1)
    declare @foundit int = 0

    if patindex(@Pattern , @string1) = 0
        return  ''

    if right(rtrim(@string1),1) <> @Delimiter
        set @string1 = @string1  + @Delimiter

    set @position =  patindex(@Pattern , @string1) + @ld  -1  
    while @position > 0
    begin
        set @counter = @counter +1 
        set @ls1  = len (@string1)
        if (@ls1 >= @ld)
            set @piece = left(@string1, @position - @ld)
        else
            break
        if (@counter = @ElementNumber)
        begin
            set @foundit = 1
                break
        end
        if len(@string1) > 0
        begin
            set @string1 = stuff(@string1, 1, @position, '')
            set @position =  patindex(@Pattern , @string1) + @ld  -1  
        end
        else
        set @position = -1
    end 


    if @foundit =1
        set @returnVal = @piece
    else 
        set @returnVal =  ''
    return @returnVal

【讨论】:

您好像在这里提问。你是?如果没有,请删除您提出问题的部分。【参考方案9】:

我没有足够的声誉来发表评论,所以我要添加一个答案。请酌情调整。

对于两个分隔符之间没有任何内容的情况,我对 Gary Kindel 的回答有疑问

如果你这样做 select * from dbo.GetSplitString_CTE('abc^def^^ghi','^',3) 你得到 吉 而不是空字符串

如果你注释掉 LEN([值]) > 0 行,你会得到想要的结果

【讨论】:

【参考方案10】:

您可以将此选择放入 UFN。如果您需要,您也可以自定义它以指定分隔符。在这种情况下,您的 ufn 将有两个输入。第 N 个数字和要使用的分隔符。

    DECLARE @tlist varchar(max)='10,20,30,40,50,60,70,80,90,100'
    DECLARE @i INT=1, @nth INT=3
    While len(@tlist) <> 0
    BEGIN
            IF @i=@nth
            BEGIN
              select Case when charindex(',',@tlist) <> 0 Then LEFT(@tlist,charindex(',',@tlist)-1)
                          Else @tlist
                    END
            END

              Select @tlist = Case when charindex(',',@tlist) <> 0 Then substring(@tlist,charindex(',',@tlist)+1,len(@tlist))
                          Else ''
                          END

            SELECT @i=@i+1
    END

【讨论】:

【参考方案11】:

这是我最初的解决方案... 它基于 Aaron Bertrand http://www.sqlperformance.com/2012/07/t-sql-queries/split-strings

的工作

我只是更改了返回类型以使其成为标量函数。

示例: SELECT dbo.GetSplitString_CTE('1,222,2,67,888,1111',',',4)

CREATE FUNCTION dbo.GetSplitString_CTE
(
   @List       VARCHAR(MAX),
   @Delimiter  VARCHAR(255),
   @ElementNumber int
)
RETURNS VARCHAR(4000)
AS
BEGIN

   DECLARE @result varchar(4000)    
   DECLARE @Items TABLE ( position int IDENTITY PRIMARY KEY,
                          Item VARCHAR(4000)
                         )  

   DECLARE @ll INT = LEN(@List) + 1, @ld INT = LEN(@Delimiter);  

   WITH a AS
   (
       SELECT
           [start] = 1,
           [end]   = COALESCE(NULLIF(CHARINDEX(@Delimiter, 
                       @List, @ld), 0), @ll),
           [value] = SUBSTRING(@List, 1, 
                     COALESCE(NULLIF(CHARINDEX(@Delimiter, 
                       @List, @ld), 0), @ll) - 1)
       UNION ALL
       SELECT
           [start] = CONVERT(INT, [end]) + @ld,
           [end]   = COALESCE(NULLIF(CHARINDEX(@Delimiter, 
                       @List, [end] + @ld), 0), @ll),
           [value] = SUBSTRING(@List, [end] + @ld, 
                     COALESCE(NULLIF(CHARINDEX(@Delimiter, 
                       @List, [end] + @ld), 0), @ll)-[end]-@ld)
       FROM a
       WHERE [end] < @ll
   )
   INSERT @Items SELECT [value]
   FROM a
   WHERE LEN([value]) > 0
   OPTION (MAXRECURSION 0);

   SELECT @result=Item
   FROM @Items
   WHERE position=@ElementNumber

   RETURN @result;
END
GO

【讨论】:

首先使用繁重的递归 CTE 来拆分您的字符串,只是为了挑选出 第 n 个元素,这是一个相当大的开销。这可以更容易地完成......

以上是关于使用 T-SQL,从字符串中返回第 n 个分隔元素的主要内容,如果未能解决你的问题,请参考以下文章

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

在c ++中输入n个空格分隔的整数

请问sas中scan函数如何使用?

008实现一个算法从一个单链表中返回倒数第n个元素(keep it up)

Linux常用命令——cut

day4