使用SQL Server,我如何分割一个字符串,以便我可以访问项目x?

拿一根“你好,约翰·史密斯”的绳子。我如何通过空格分割字符串并访问索引1应该返回“约翰”的项目?


当前回答

这个问题不是关于字符串分割方法,而是关于如何获取第n个元素。

这里所有的答案都是使用递归,CTEs,多个CHARINDEX, REVERSE和PATINDEX,发明函数,调用CLR方法,数字表,CROSS APPLYs…大多数答案都包含了很多行代码。

但是-如果你真的只想要一个方法来获取第n个元素-这可以作为真正的一行程序来完成,没有UDF,甚至没有子选择…作为一个额外的好处:类型安全

用空格分隔第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)');

更新SQL-Server 2016+

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

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

对于OPENJSON,文档清楚地说明:

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

像1,2,3这样的字符串只需要括号:[1,2,3]。 像这样的一串单词需要是["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 -获取类型安全的值

只需使用double[[]],就可以在数组中使用数组。这允许使用类型化的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

其他回答

在Azure SQL数据库(基于Microsoft SQL Server但不完全相同的东西)中,STRING_SPLIT函数的签名看起来像这样:

STRING_SPLIT ( string , separator [ , enable_ordinal ] )

当enable_ordinal标志设置为1时,结果将包括一个名为ordinal的列,该列由输入字符串中子字符串的基于1的位置组成:

SELECT *
FROM STRING_SPLIT('hello john smith', ' ', 1)

| value | ordinal |
|-------|---------|
| hello | 1       |
| john  | 2       |
| smith | 3       |

这允许我们这样做:

SELECT value
FROM STRING_SPLIT('hello john smith', ' ', 1)
WHERE ordinal = 2

| value |
|-------|
| john  |

如果enable_ordinal不可用,则有一个技巧,即假定输入字符串中的子字符串是惟一的。在这种情况下,CHAR_INDEX可以用来查找子字符串在输入字符串中的位置:

SELECT value, ROW_NUMBER() OVER (ORDER BY CHARINDEX(value, input_str)) AS ord_pos
FROM (VALUES
    ('hello john smith')
) AS x(input_str)
CROSS APPLY STRING_SPLIT(input_str, ' ')

| value | ord_pos |
|-------+---------|
| hello | 1       |
| john  | 2       |
| smith | 3       |

试试这个:

CREATE function [SplitWordList]
(
 @list varchar(8000)
)
returns @t table 
(
 Word varchar(50) not null,
 Position int identity(1,1) not null
)
as begin
  declare 
    @pos int,
    @lpos int,
    @item varchar(100),
    @ignore varchar(100),
    @dl int,
    @a1 int,
    @a2 int,
    @z1 int,
    @z2 int,
    @n1 int,
    @n2 int,
    @c varchar(1),
    @a smallint
  select 
    @a1 = ascii('a'),
    @a2 = ascii('A'),
    @z1 = ascii('z'),
    @z2 = ascii('Z'),
    @n1 = ascii('0'),
    @n2 = ascii('9')
  set @ignore = '''"'
  set @pos = 1
  set @dl = datalength(@list)
  set @lpos = 1
  set @item = ''
  while (@pos <= @dl) begin
    set @c = substring(@list, @pos, 1)
    if (@ignore not like '%' + @c + '%') begin
      set @a = ascii(@c)
      if ((@a >= @a1) and (@a <= @z1))  
        or ((@a >= @a2) and (@a <= @z2))
        or ((@a >= @n1) and (@a <= @n2))
      begin
        set @item = @item + @c
      end else if (@item > '') begin
        insert into @t values (@item)
        set @item = ''
      end
    end 
    set @pos = @pos + 1
  end
  if (@item > '') begin
    insert into @t values (@item)
  end
  return
end

像这样测试它:

select * from SplitWordList('Hello John Smith')

首先,创建一个函数(使用CTE,公共表表达式不再需要临时表)

 create function dbo.SplitString 
    (
        @str nvarchar(4000), 
        @separator char(1)
    )
    returns table
    AS
    return (
        with tokens(p, a, b) AS (
            select 
                1, 
                1, 
                charindex(@separator, @str)
            union all
            select
                p + 1, 
                b + 1, 
                charindex(@separator, @str, b + 1)
            from tokens
            where b > 0
        )
        select
            p-1 zeroBasedOccurance,
            substring(
                @str, 
                a, 
                case when b > 0 then b-a ELSE 4000 end) 
            AS s
        from tokens
      )
    GO

然后,像这样使用它作为任何表(或修改它以适应现有存储的proc)。

select s 
from dbo.SplitString('Hello John Smith', ' ')
where zeroBasedOccurance=1

更新

以前的版本将失败的输入字符串长度超过4000个字符。这个版本考虑到了以下限制:

create function dbo.SplitString 
(
    @str nvarchar(max), 
    @separator char(1)
)
returns table
AS
return (
with tokens(p, a, b) AS (
    select 
        cast(1 as bigint), 
        cast(1 as bigint), 
        charindex(@separator, @str)
    union all
    select
        p + 1, 
        b + 1, 
        charindex(@separator, @str, b + 1)
    from tokens
    where b > 0
)
select
    p-1 ItemIndex,
    substring(
        @str, 
        a, 
        case when b > 0 then b-a ELSE LEN(@str) end) 
    AS s
from tokens
);

GO

用法不变。

几乎所有其他答案都是替换正在分割的字符串,这浪费了CPU周期并执行不必要的内存分配。

我在这里介绍了一种更好的进行字符串拆分的方法:http://www.digitalruby.com/split-string-sql-server/

代码如下:

SET NOCOUNT ON

-- You will want to change nvarchar(MAX) to nvarchar(50), varchar(50) or whatever matches exactly with the string column you will be searching against
DECLARE @SplitStringTable TABLE (Value nvarchar(MAX) NOT NULL)
DECLARE @StringToSplit nvarchar(MAX) = 'your|string|to|split|here'
DECLARE @SplitEndPos int
DECLARE @SplitValue nvarchar(MAX)
DECLARE @SplitDelim nvarchar(1) = '|'
DECLARE @SplitStartPos int = 1

SET @SplitEndPos = CHARINDEX(@SplitDelim, @StringToSplit, @SplitStartPos)

WHILE @SplitEndPos > 0
BEGIN
    SET @SplitValue = SUBSTRING(@StringToSplit, @SplitStartPos, (@SplitEndPos - @SplitStartPos))
    INSERT @SplitStringTable (Value) VALUES (@SplitValue)
    SET @SplitStartPos = @SplitEndPos + 1
    SET @SplitEndPos = CHARINDEX(@SplitDelim, @StringToSplit, @SplitStartPos)
END

SET @SplitValue = SUBSTRING(@StringToSplit, @SplitStartPos, 2147483647)
INSERT @SplitStringTable (Value) VALUES(@SplitValue)

SET NOCOUNT OFF

-- You can select or join with the values in @SplitStringTable at this point.

这个问题不是关于字符串分割方法,而是关于如何获取第n个元素。

这里所有的答案都是使用递归,CTEs,多个CHARINDEX, REVERSE和PATINDEX,发明函数,调用CLR方法,数字表,CROSS APPLYs…大多数答案都包含了很多行代码。

但是-如果你真的只想要一个方法来获取第n个元素-这可以作为真正的一行程序来完成,没有UDF,甚至没有子选择…作为一个额外的好处:类型安全

用空格分隔第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)');

更新SQL-Server 2016+

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

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

对于OPENJSON,文档清楚地说明:

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

像1,2,3这样的字符串只需要括号:[1,2,3]。 像这样的一串单词需要是["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 -获取类型安全的值

只需使用double[[]],就可以在数组中使用数组。这允许使用类型化的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