【问题标题】:Using T-SQL, return nth delimited element from a string使用 T-SQL,从字符串中返回第 n 个分隔元素
【发布时间】: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

【问题讨论】:

    标签: sql-server sql-server-2008 tsql split


    【解决方案1】:

    您可以将 STRING_SPLIT 与 ROW_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_VALUE 和 OPENJSON。

    使用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 characters 或 markup... 时,字符串总是很棘手
    【解决方案4】:

    或者,可以使用xml、nodes() 和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 个元素,这是一个相当大的开销。这可以更容易地完成......
            猜你喜欢
            • 2022-01-18
            • 1970-01-01
            • 2018-08-09
            • 2021-12-17
            • 2011-07-14
            • 2014-06-20
            相关资源
            最近更新 更多