SQL - STRING_SPLIT 字符串位置

4

我有一个包含两列逗号分隔字符串的表格。根据数据格式,这两列中逗号分隔项的数量相等,colA 中的第一个值与 colB 中的第一个值相关联,以此类推。(显然,这不是一种非常好的数据格式,但这是我正在使用的格式。)

如果我有以下行(PrimaryKeyID | column1 | column2):

1 | a,b,c | A,B,C

那么在这种数据格式中,a&1、b&2等是逻辑相关的。

我想使用STRING_SPLIT来拆分这些列,但显然使用两次会交叉它们,导致总共有9行。

1 | a | A
1 | b | A    
1 | c | A    
1 | a | B    
1 | b | B    
1 | c | B    
1 | a | C
1 | b | C    
1 | c | C

我想要的只是三个“逻辑相关”的列。
1 | a | A
1 | b | B
1 | c | C

然而,STRING_SPLIT(myCol,',') 似乎没有保存字符串位置。

我已经执行了以下操作:

SELECT tbl.ID,
      t1.Column1Value,
      t2.Column2Value
FROM myTable tbl
INNER JOIN (
   SELECT t.ID, 
       ss.value AS Column1Value, 
       ROW_NUMBER() OVER (PARTITION BY t.ID ORDER BY t.ID) as StringOrder
   FROM myTable t
   CROSS APPLY STRING_SPLIT(t.column1,',') ss
) t1 ON tbl.ID = t1.ID
INNER JOIN (
   SELECT t.ID, 
       ss.value AS Column2Value, 
       ROW_NUMBER() OVER (PARTITION BY t.ID ORDER BY t.ID) as StringOrder
   FROM myTable t
   CROSS APPLY STRING_SPLIT(t.column2,',') ss
) t1 ON tbl.ID = t2.ID AND t1.StringOrder = t2.StringOrder

这在我的小测试集上似乎有效,但我认为没有理由期望它每次都能保证有效。显然,ROW_NUMBER() OVER (PARTITION BY ID ORDER BY ID) 是无意义的排序,但似乎在没有任何实际排序的情况下,STRING_SPLIT 返回的值是按照它们已经存在的“默认”顺序返回的。这是“预期”的行为吗?我可以依靠它吗?还有其他方法可以完成我尝试做的事情吗?
谢谢。
======================
编辑
我使用以下 UDF 得到了我想要的东西(我想)。但是它非常慢。有什么建议吗?
CREATE FUNCTION fn.f_StringSplit(@string VARCHAR(MAX),@delimiter VARCHAR(1))
RETURNS @r TABLE
(
    Position INT,
    String VARCHAR(255)
)
AS
BEGIN

    DECLARE @current_position INT
    SET @current_position = 1

    WHILE CHARINDEX(@delimiter,@string) > 0 BEGIN

        INSERT INTO @r (Position,String) VALUES (@current_position, SUBSTRING(@string,1,CHARINDEX(@delimiter,@string) - 1))

        SET @current_position = @current_position + 1
        SET @string = SUBSTRING(@string,CHARINDEX(@delimiter,@string) + 1, LEN(@string) - CHARINDEX(@delimiter,@string))

    END

    --add the last one
    INSERT INTO @r (Position, String) VALUES(@current_position,@string)

    RETURN
END

1
这是我对STRING_SPLIT的最大抱怨。如果顺序很重要,那就麻烦了。当然,最好的解决方案是不存储分隔数据!但是,由于并非总是可行,您可能需要考虑其他一些分割工具。Jeff Moden提供的这个工具包括每个值的序号位置。http://www.sqlservercentral.com/articles/Tally+Table/72993/ - Sean Lange
每个字符串中总是有3个项目吗? - Aaron Dietz
不一定是3个项目,理论上可以是任意数量的项目,尽管通常是1-25个。 - mark
关于预期行为:它说输出行的顺序可能是任意的。不能保证顺序与输入字符串中子字符串的顺序相匹配。 - Salman A
1
string_split函数已更新,添加了一个标志来包含序数。 https://learn.microsoft.com/en-us/sql/t-sql/functions/string-split-transact-sql - Simon Zeinstra
8个回答

17

我发现保持 String_Split() 函数的顺序表达的唯一方法是使用 Row_Number() 函数,并在“order by”中使用一个字面值。

例如:

declare @Version nvarchar(128)
set @Version = '1.2.3';

with V as (select value v, Row_Number() over (order by (select 0)) n from String_Split(@Version, '.'))
    select
        (select v from V where n = 1) Major,
        (select v from V where n = 2) Minor,
        (select v from V where n = 3) Revision

返回:

Major Minor Revision
----- ----- ---------
1     2     3        

更新:如果您使用较新版本的SQL Server,现在可以提供一个可选的第三个位参数,该参数表示结果中还应包括一个序号列。有关更多详细信息,请参见我的其他答案。


但是如果对于STRING_SPLIT()函数来说,“顺序不保证”,那么什么能确保Row_Number()函数返回正确的原始顺序呢? - T N
是的,但你会注意到这并不比其他选项更糟糕,这些选项都依赖于内部返回顺序,但表达能力有限。幸运的是,现在有一种“正确”的方法;请查看我的更新评论。 - N8allan

4

幸运的是,在更新的SQL Server(Azure和2022)中,新增了一个可选标志来包括一个“ordinal”列。如果您正在使用较新版本的SQL Server,则这终于提供了一个符合逻辑正确性而非实现特定性的解决方案。

新定义:

String_Split(string, separator [, enable_ordinal])
e.g. String_Split('1.2.3', '.', 1)

例子:

with V as (select Value v, Ordinal n from String_Split('1.2.3', '.', 1))
    select
        (select v from V where n = 1) Major,
        (select v from V where n = 2) Minor,
        (select v from V where n = 3) Revision

返回:

Major Minor Revision
----- ----- ---------
1     2     3        

1
终于!这是对SQL Server的一个很棒的补充,使得我最初的问题变得微不足道。 - mark

2

您的想法很好,但您的order by没有使用稳定排序。我认为更安全的做法是:

SELECT tbl.ID, t1.Column1Value, t2.Column2Value
FROM myTable tbl INNER JOIN
     (SELECT t.ID, ss.value AS Column1Value, 
             ROW_NUMBER() OVER (PARTITION BY t.ID
                                ORDER BY CHARINDEX(',' + ss.value + ',', ',' + t.column1 + ',')
                               ) as StringOrder
      FROM myTable t CROSS APPLY
           STRING_SPLIT(t.column1,',') ss
     ) t1
     ON tbl.ID = t1.ID INNER JOIN
     (SELECT t.ID, ss.value AS Column2Value, 
             ROW_NUMBER() OVER (PARTITION BY t.ID
                                ORDER BY CHARINDEX(',' + ss.value + ',', ',' + t.column2 + ',')
                               ) as StringOrder
      FROM myTable t CROSS APPLY
           STRING_SPLIT(t.column2, ',') ss
     ) t2
     ON tbl.ID = t2.ID AND t1.StringOrder = t2.StringOrder;

注意:如果字符串具有非相邻的重复项,则可能无法按预期工作。

1
谢谢您的回复。字符串可以包含非连续的重复项(例如:1,2,1,1,1,2),但除此之外,这是一个非常优雅的解决方案! - mark

1

马克,这是我会使用的解决方案。假设您表中的[column 1]具有更稳定的“键”值,而[column2]具有相应的“字段”值,有时可以省略或为NULL:

  • 将进行两个提取,一个用于[column 1] - 我假设它是Key,另一个用于[column 2] - 我假设它是“key”的“值”,然后它们将由STRING_SPLIT函数自动解析。

  • 这两个独立的结果集将根据操作时间(始终是顺序的)重新编号。请注意,我们不是按字段内容或逗号位置等重新编号,而是按时间戳重新编号。

  • 然后它们将通过LEFT OUTER JOIN连接在一起;注意不是通过INNER JOIN连接,因为我们的“字段值”可能会被省略,而“键”始终存在

以下是TSQL代码,作为我第一篇发布到此网站的帖子,希望看上去没问题:

SELECT T1.ID, T1.KeyValue, T2.FieldValue
from (select t1.ID, row_number() OVER (PARTITION BY t1.ID ORDER BY current_timestamp) AS KeyRow, t2.value AS KeyValue 
from myTable t1
CROSS APPLY STRING_SPLIT(t1.column1,',')  as t2) T1
LEFT OUTER JOIN
(select t1.ID, row_number() OVER (PARTITION BY t1.ID ORDER BY current_timestamp) AS FieldRow, t3.value AS FieldValue 
from myTable t1
CROSS APPLY STRING_SPLIT(t1.column2,',')  as t3) T2 ON T1.ID = T2.ID AND T1.KeyRow = T2.FieldRow

这非常简单 - Amir Reza

1
我有点晚回答这个问题,但我刚尝试使用string_split做同样的事情,因为我最近遇到了性能问题。我的T-SQL字符串分割器经验使我在包含少于1,000个分隔值的大多数事情中使用递归CTE。如果需要字符串拆分中的序数,则理想情况下应使用CLR过程。

话虽如此,我对从string_split获取序数得出了与您类似的结论。您可以查看以下查询和统计信息,依次是裸string_split函数、string_split的CTE RowNumber,以及我从awesome write-up派生的个人字符串拆分CTE函数。我基于他们的多语句TVF实现将其作为内联TVF,您可以在here了解有关差异的信息。

在我的实验中,使用ROW_NUMBER对返回定界字符串内部顺序的常量没有发现偏差,因此我会继续使用它,直到我找到问题为止。但是,如果在业务环境中顺序至关重要,我可能会建议使用第一个链接中提到的Moden分割器,该链接指向作者的文章here,因为它与使用RowNumber方法的不太安全的string_split看到的性能相一致。
set nocount on;

declare
    @iter int = 0,
    @rowcount int,
    @val varchar(max) = '';

while len(@val) < 1e6
    select
        @val += replicate(concat(@iter, ','), 8e3),
        @iter += 1;

raiserror('Begin string_split Built-In', 0, 0) with nowait;

set statistics time, io on;

select
    *
from
    string_split(@val, ',')
where
    [value] > '';

select
    @rowcount = @@rowcount;

set statistics time, io off;

print '';
raiserror('End string_split Built-In | Return %d Rows', 0, 0, @rowcount) with nowait;
print '';
raiserror('Begin string_split Built-In with RowNumber', 0, 0) with nowait;

set statistics time, io on;

with cte
as  (
    select
        *,
        [group] = 1
    from
        string_split(@val, ',')
    where
        [value] > ''
    ),
    cteCount
as  (
    select
        *,
        [id] = row_number() over (order by [group])
    from
        cte
    )
select
    *
from
    cteCount;

select
    @rowcount = @@rowcount;

set statistics time, io off;

print '';
raiserror('End string_split Built-In with RowNumber | Return %d Rows', 0, 0, @rowcount) with nowait;
print '';
raiserror('Begin Moden String Splitter', 0, 0) with nowait;

set statistics time, io on;

select
    *
from
    dbo.SplitStrings_Moden(@val, ',')
where
    item > '';

select
    @rowcount = @@rowcount;

set statistics time, io off;

print '';
raiserror('End Moden String Splitter | Return %d Rows', 0, 0, @rowcount) with nowait;
print '';
raiserror('Begin Recursive CTE String Splitter', 0, 0) with nowait;

set statistics time, io on;

select
    *
from
    dbo.fn_splitByDelim(@val, ',')
where
    strValue > ''
option
    (maxrecursion 0);

select
    @rowcount = @@rowcount;

set statistics time, io off;

统计学是

Begin string_split Built-In

 SQL Server Execution Times:
   CPU time = 2000 ms,  elapsed time = 5325 ms.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.

End string_split Built-In | Return 331940 Rows

Begin string_split Built-In with RowNumber

 SQL Server Execution Times:
   CPU time = 2094 ms,  elapsed time = 8119 ms.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.

End string_split Built-In with RowNumber | Return 331940 Rows

Begin Moden String Splitter
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 6 ms.

 SQL Server Execution Times:
   CPU time = 8734 ms,  elapsed time = 9009 ms.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.

End Moden String Splitter | Return 331940 Rows

Begin Recursive CTE String Splitter
Table 'Worktable'. Scan count 2, logical reads 1991648, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 147188 ms,  elapsed time = 147480 ms.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.

End Recursive CTE String Splitter | Return 331940 Rows

1
这非常简单 - Amir Reza

1
SELECT
PrimaryKeyID ,t2.items as column1, t1.items as column2 from [YourTableName]
cross Apply [dbo].[Split](column2) as t1
cross Apply [dbo].[Split](column1) as t2

0
这是一个使用string_split函数并添加序号列的T-SQL函数:
drop function if exists [dbo].[varchar_split2];
go
create function [dbo].[varchar_split2]
(
    @text varchar(max),
    @delimiter char(1) = ','
)
returns @result table ([Ordinal] int not null identity(1, 1) primary key, [Value] varchar(128) not null)
as
begin
    insert @result ([Value])
    select
        [Value]
    from
        string_split(@text, @delimiter)
    where
        0 != len([Value])
    ;
    return;
end;
go

0

这非常简单

CREATE TABLE #a(
id [INT] IDENTITY(1,1) NOT NULL,
OrgId INT )

INSERT INTO #a
(
    OrgId
)
 SELECT  value FROM STRING_SPLIT('18,44,45,46,47,48,49,50,51,52,53', ',')

Select * from #a

问题在于非Azure版本的STRING_SPLIT既没有提供序号选项,也不能保证结果顺序。使用IDENTITY或ROW_NUMBER来分配序号的各种技术在测试时可能看起来有效,但根据文档行为无法证明其正确性。 - T N

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接