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

时间:2013-10-18 12:18:15

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

我需要创建一个函数,它将返回分隔字符串的第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

11 个答案:

答案 0 :(得分:20)

这是最简单的答案来缓解67(类型安全!! ):

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

这个问题不是关于字符串拆分方法,而是如何获取第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: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)')

如果您的字符串可能包含禁止字符,您仍然可以这样做。首先在字符串上使用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)');

答案 1 :(得分:7)

这是我最初的解决方案...... 它基于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

答案 2 :(得分:6)

怎么样:

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

答案 3 :(得分: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

       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

答案 4 :(得分:2)

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

    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

答案 5 :(得分:2)

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

@p - 所需的位置(1,2,3 ...)

@d - 分隔符('/')

trim(substring(replace(@ a,@ d,replicate('',len(@a))),(@ p-1)* len(@a)+ 1,len(@a)))

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

来自https://exceljet.net/formula/split-text-with-delimiter

的文章

完全基于

答案 6 :(得分:1)

我宁愿创建一个带有标识列的临时表,并用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)

请参见下面的快照:

see the snapshot

答案 7 :(得分:0)

由于我的声誉很低,我不能评论加里的解决方案

我知道加里正在引用另一个链接。

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

@ld INT = LEN(@Delimiter)

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

我使用单个字符分隔符测试了许多示例,并且它们有效。大多数情况下,分隔符是单个字符。但是,由于开发人员将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

答案 8 :(得分:0)

或者,可以使用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;

答案 9 :(得分:0)

您可以将 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'

答案 10 :(得分:-1)

我没有足够的声誉来评论,所以我正在添加一个答案。请根据需要进行调整。

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

如果你这样做 从dbo.GetSplitString_CTE中选择*(&#39; abc ^ def ^^ ghi&#39;,&#39; ^&#39;,3) 你得到 GHI 而不是空字符串

如果你注释掉了  LERE([值])&gt; 0 ,你得到了理想的结果