14

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

4

11 回答 11

36

这是恢复 67 (类型安全!!)的最简单答案:

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

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

像这样的字符串1,2,3只需要括号:[1,2,3].
一串单词 like this is an exampleneed to be ["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

这篇文章中,我测试了各种方法,发现OPENJSON速度非常快。甚至比著名的“delimitedSplit8k()”方法还要快...

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

我们可以简单地通过使用 doubled 来在数组中[[]]使用数组。这允许一个类型化的WITH子句:

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
于 2016-07-08T19:53:59.347 回答
8

怎么样:

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
于 2018-05-02T08:59:11.130 回答
5

这是我最初的解决方案...它基于 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
于 2013-10-18T12:18:14.520 回答
3

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

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

@d - 分隔符('/')

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

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

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

于 2018-05-29T19:01:50.743 回答
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
于 2013-10-18T12:51:54.177 回答
1

you can put this select into UFN. if you need you can customize it for specifying delimiter as well. in that case your ufn will have two input. number Nth and delimiter to use.

    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
于 2013-10-18T13:54:51.170 回答
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)

请参阅下面的快照:

看快照

于 2019-03-29T00:14:05.053 回答
0

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

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

如果你确实 select * from dbo.GetSplitString_CTE('abc^def^^ghi','^',3) 你会得到 ghi 而不是空字符串

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

于 2015-05-31T17:36:02.737 回答
0

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

于 2020-10-28T20:56:38.623 回答
0

由于我的声誉低,我无法评论 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
于 2015-10-03T01:49:49.350 回答
0

或者,可以使用xml和。我们可以根据元素的文档顺序对元素进行排序。例如:nodes()ROW_NUMBER

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;
于 2019-06-08T06:05:36.560 回答