【问题标题】:Search all positions of char in string and return as comma separated string搜索字符串中 char 的所有位置并以逗号分隔的字符串返回
【发布时间】:2016-02-02 08:30:20
【问题描述】:

我的字符串 (VARCHAR(255)) 只包含零或一。
我需要搜索所有位置并将它们作为逗号分隔的字符串返回。 我使用来自https://dba.stackexchange.com/questions/41961/how-to-find-all-positions-of-a-string-within-another-string的解决方案构建了两个查询

到目前为止,这是我的代码:

DECLARE @TERM VARCHAR(5);
SET @TERM = '1';
DECLARE @STRING VARCHAR(255);
SET @STRING = '101011011000000000000000000000000000000000000000';

DECLARE @RESULT VARCHAR(100);
SET @RESULT = '';

SELECT
   @RESULT = @RESULT + CAST(X.pos AS VARCHAR(10)) + ','
FROM
   ( SELECT
      pos = Number - LEN(@TERM)
     FROM
      ( SELECT
         Number
        ,Item = LTRIM(RTRIM(SUBSTRING(@STRING, Number, CHARINDEX(@TERM, @STRING + @TERM, Number) - Number)))
        FROM
         ( SELECT ROW_NUMBER () OVER (ORDER BY [object_id]) FROM sys.all_objects
         ) AS n ( Number )
        WHERE
         Number > 1
         AND Number <= CONVERT(INT, LEN(@STRING))
         AND SUBSTRING(@TERM + @STRING, Number, LEN(@TERM)) = @TERM
      ) AS y
   ) X;

SELECT
   SUBSTRING(@RESULT, 0, LEN(@RESULT));



DECLARE @POS INT;
DECLARE @OLD_POS INT;
DECLARE @POSITIONS VARCHAR(100);
SELECT
   @POSITIONS = '';
SELECT
   @OLD_POS = 0;
SELECT
   @POS = PATINDEX('%1%', @STRING); 
WHILE @POS > 0
   AND @OLD_POS <> @POS
   BEGIN
      SELECT
         @POSITIONS = @POSITIONS + CAST(@POS AS VARCHAR(2)) + ',';
      SELECT
         @OLD_POS = @POS;
      SELECT
         @POS = PATINDEX('%1%', SUBSTRING(@STRING, @POS + 1, LEN(@STRING))) + @POS;
   END;
SELECT
   LEFT(@POSITIONS, LEN(@POSITIONS) - 1);

我想知道这是否可以更快/更好地完成?我只搜索单个字符位置,我的字符串中只有两个字符(0 和 1)。

我已经使用这段代码构建了两个函数,运行它们 1000 条记录并在同一时间得到相同的结果,所以我不知道哪个更好。

对于单条记录,第二部分在 Profiler 中给出 CPU 和读取等于 0,其中第一段代码给我 CPU=16 和读取=17。

我需要得到如下所示的结果:1,3,5,6,8,9(多次出现时),3 表示单次出现,NONE 如果没有出现。

【问题讨论】:

  • 必须在SQL中完成吗?识别所有1 的职位的目的是什么?
  • 理想情况下,更改所需的输出,以及可能的存储设计。如果您需要处理多个值,则填充到字符串中的逗号分隔值应该是 last 的手段。 SQL Server 具有设计 用于保存多个值的类型,例如 tables 和 XML。
  • @DStanley 我有一个包含选项的表。有 100 列。这是一个非常古老的数据库。行只能有单个值,例如行有单个 1,但旧行有错误。我正在创建搜索这些行并列出它们的报告。我已经将值转换为 01 字符串,现在我需要知道这些值的位置才能获得选项编号。
  • @Damien_The_Unbeliever 我无法更改输出,这是我得到的要求之一。我必须以我的问题中描述的格式显示具有无效值的行(每行超过一个 1)
  • @Misiu 如果有可能在应用层这样做,那会容易得多。 SQL 不是为那些类型的非基于集合的操作而设计的。

标签: sql sql-server tsql sql-server-2005


【解决方案1】:

一些tally 表和xml 解决方案:

DECLARE @STRING NVARCHAR(100) = '101011011000000000000000000000000000000000000000';

;with cte as(select ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) p 
             from (values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) t1(n) cross join
                  (values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) t2(n) cross join
                  (values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) t3(n))
SELECT STUFF((SELECT  ',' + CAST(p AS VARCHAR(100))
              FROM cte
              WHERE p <= LEN(@STRING) AND SUBSTRING(@STRING, p, 1) = '1'
              FOR XML PATH('')), 1, 1, '')

您只需生成从 1 到 1000 的数字(如果字符串长度更大,则添加更多连接)并使用 substring 函数过滤所需的值。然后将行连接到逗号分隔值的标准技巧。

对于旧版本:

;with cte as(SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) p
             FROM sys.all_columns a CROSS JOIN sys.all_columns b)
SELECT STUFF((SELECT  ',' + CAST(p AS VARCHAR(100))
              FROM cte
              WHERE p <= LEN(@STRING) AND SUBSTRING(@STRING, p, 1) = '1'
              FOR XML PATH('')), 1, 1, '')

这是一篇关于生成范围的好文章http://dwaincsql.com/2014/03/27/tally-tables-in-t-sql/

编辑:

;with cte as(SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) p
               FROM (SELECT 1 AS rn UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 ) t1 CROSS JOIN 
                (SELECT 1 AS rn UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 ) t2 CROSS JOIN 
                (SELECT 1 AS rn UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 ) t3 CROSS JOIN 
                (SELECT 1 AS rn UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 ) t4 CROSS JOIN 
                (SELECT 1 AS rn UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 ) t5 CROSS JOIN 
                (SELECT 1 AS rn UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 ) t6)

【讨论】:

  • 感谢您如此快速的回复,但我无法在 SQL Server 2005 上运行它。我收到错误:Incorrect syntax near the keyword 'values'.
  • 现在可以正常使用了,谢谢。您能否用几句话描述为什么这比我的两个解决方案更好/更快?这三个都可以,但我不能说哪个更好
  • 游标比基于集合的语句慢。此外,您不应该仅仅因为语法允许您这样做而使用游标。每当您可以设法不使用光标时,最好这样做。
  • 我刚刚比较了@josephstyons 解决方案,它速度更快,CPU 和读取更少。我已经在 140k 行上对此进行了测试。
  • @Misiu,交叉连接会给你一个笛卡尔。在示例中,我有 5 个select 1s,因此行数将为 5*5*5*5*5*5 = 15625。对于您可能拥有的任何字符串都足够了。只是有一些大量的行。但是您正在按长度过滤外部,因此如果添加更多连接不会影响性能。优化器足够聪明。
【解决方案2】:

Giorgi 的反应非常聪明,但我更喜欢更老式的、更易读的方法。我的建议,包括测试用例:

if object_id('UFN_CSVPOSITIONS') is not null
begin
  drop function ufn_csvpositions;
end
go

create function dbo.UFN_CSVPOSITIONS
(
  @string nvarchar(255)
 ,@delimiter nvarchar(1) = ','
)
returns nvarchar(255)
as
begin
  --given a string that contains ones,
  --return a comma-delimited list of the positions of those ones
  --example: '1001' returns '1,4'
  declare @result nvarchar(255) = '';
  declare @i int = 1;
  declare @slen int = len(@string);
  declare @idx int = 0;

  while @i < @slen
  begin
    set @idx = charindex('1',@string,@i);
    if 0 = @idx
    begin
      set @i = @slen;  --no more to be found, break out early
    end
    else
    begin
      set @result = @result + @delimiter + convert(nvarchar(3),@idx);
      set @i = @idx; --jump ahead
    end;
    set @i = @i + 1;
  end  --while

  if (0 < len(@result)) and (',' = substring(@result,1,1))
  begin
    set @result = substring(@result,2,len(@result)-1)
  end

  return @result;
end
go

--test cases
DECLARE @STRING NVARCHAR(255) = '';
set @string = '101011011000000000000000000000000000000000000000';
print dbo.UFN_CSVPOSITIONS(@string,',');
set @string = null;
print dbo.UFN_CSVPOSITIONS(@string,',');
set @string = '';
print dbo.UFN_CSVPOSITIONS(@string,',');
set @string = '1111111111111111111111111111111111111111111111111';
print dbo.UFN_CSVPOSITIONS(@string,',');
set @string = '0000000000000000000000000000000000000000000000000';
print dbo.UFN_CSVPOSITIONS(@string,',');

--lets try a very large # of test cases, see how fast it comes out
--255 "ones" should be the worst case scenario for performance, so lets run through 50k of those.
--on my laptop, here are test case results:
--all 1s   : 13 seconds
--all 0s   :  7 seconds
--all nulls:  1 second
declare @testinput nvarchar(255) = replicate('1',255);
declare @iterations int = 50000;
declare @i int = 0;
while @i < @iterations
begin
  print dbo.ufn_csvpositions(@testinput,',');
  set @i = @i + 1;
end;

--repeat the test using the CTE method.
--the same test cases are as follows on my local:
--all 1s   : 18 seconds
--all 0s   : 15 seconds
--all NULLs: 1  second
set nocount on;
set @i = 0;
set @iterations = 50000;
declare @result nvarchar(255) = '';
set @testinput = replicate('1',255);
while @i < @iterations
begin
  ;with cte as(SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) p
               FROM sys.all_columns a CROSS JOIN sys.all_columns b)
  SELECT @result = STUFF((SELECT  ',' + CAST(p AS VARCHAR(100))
                FROM cte
                WHERE p <= LEN(@testinput) AND SUBSTRING(@testinput, p, 1) = '1'
                FOR XML PATH('')), 1, 1, '')
  print @result;
  set @i = @i + 1;
end;

【讨论】:

  • 我有使用 while 循环的解决方案,但感谢您的回答和花费的时间。我正在寻找最快的方法来做到这一点。您的代码更易于阅读,但我认为 @giorgi-nakeuri 代码要快得多。对不起,如果我错了,但如果涉及到 SQL 查询优化,我是新手。
  • 当我比较两种方法的性能时,WHILE 循环更快。我已经编辑了答案以包含两种方法的测试用例;在您的机器上试用一下,看看您的体验是否相同。
  • 我必须确认带有 WHILE 的版本要快一点。我已经用 WHILE 方法测试了 140k 行和 CPU 和读取更小。我必须在更大的数据集上进行测试。
  • @JosephStyons,这是因为从 sys 表中选择...查看我的答案以获取另一个版本的计数并检查。现在我的胜过你的……
  • 我也可以确认 - 在我的机器上,执行 50,000 个“全 1”字符串需要 11 秒,而之前需要 18 秒,而使用 while 循环则需要 13 秒。我坚持我的断言,即代码的清晰度值得性能的一小部分损失,但这是由 OP 决定的权衡。无论如何,最令人印象深刻的!
猜你喜欢
  • 1970-01-01
  • 2016-08-30
  • 1970-01-01
  • 1970-01-01
  • 2011-10-17
  • 2016-04-24
  • 2012-11-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多