【问题标题】:Convert strings to integers using PatIndex使用 PatIndex 将字符串转换为整数
【发布时间】:2017-02-15 14:16:58
【问题描述】:

我想从相当复杂的字符串中返回整数,这些字符串将 unicode 字符(例如 -.)与字符和整数组合在一起。

在实现这一点上我已经走了很长一段路,但我仍然遇到一些结构更复杂的字符串的问题。例如:

DECLARE @Tabl as table
(
   dats nvarchar(15)
)

INSERT INTO @Tabl VALUES
('103-P705hh'),
('115-xxx-44'),
('103-705.13'),
('525-hheef4')

select LEFT(SUBSTRING(REPLACE(REPLACE(dats, '.',''),'-',''), PATINDEX('%[0-9.-]%', REPLACE(REPLACE(dats, '.',''),'-','')), 8000),
       PATINDEX('%[^0-9.-]%', SUBSTRING(REPLACE(REPLACE(dats, '.',''),'-',''), PATINDEX('%[0-9.-]%', REPLACE(REPLACE(dats, '.',''),'-','')), 8000) + 'X')-1)
from @tabl

给予

Raw Input          Actual return:          Desired return:
103-P705hh         103                     103705
115-xxx-44         115                     11544
103-705.13         10370513                10370513
525-hheef4         525                     5254

我昨天有一个关于这个的话题来讨论存在多个-. 的情况,但从返回中可以看出,这实际上现在已经处理好了。但是,在扩展我使用的数据库时,我遇到了更复杂的字符串,例如我在这里介绍的那些。

有没有人知道当字符和整数在字符串中“混合”时该怎么办?

问候, 森德泽

【问题讨论】:

标签: sql-server string


【解决方案1】:

我见过大量使用带有循环的标量 udf 的解决方案,但我不喜欢其中任何一种,所以用不同的方法将我的帽子扔进戒指。

在数字表的帮助下,您可以将每个值解构为单独的字符,删除非数字字符,然后使用FOR XML 重构它以连接行,例如

WITH Numbers (Number) AS
(   SELECT  ROW_NUMBER() OVER(ORDER BY N1.N)
    FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N1 (N)         -- 100
    CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N2 (N)   -- 100
    CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N3 (N)   -- 1,000
    --CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N4 (N)   -- 10,000
    --CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N5 (N) -- 100,000

    --COMMENT OR UNCOMMENT ROWS AS NECESSARY DEPENDING ON YOU MAX STRING LENGTH
)
SELECT  t.dats,
        Stripped = x.data.value('.', 'INT')
FROM    @tabl AS t
        CROSS APPLY
        (   SELECT  SUBSTRING(t.dats, n.Number, 1)
            FROM    Numbers n
            WHERE   n.Number <= LEN(t.dats)
            AND     SUBSTRING(t.dats, n.Number, 1) LIKE '[0-9]'
            ORDER BY n.Number
            FOR XML PATH(''), TYPE
        ) x (data);

给予:

dats            Stripped
----------------------
103-P705hh      103705
115-xxx-44      11544
103-705.13      10370513
525-hheef4      5254

我没有进行任何测试,因此将每个字符串扩展为单个字符并重新构建它所增加的开销实际上比带有循环的 UDF 开销要大得多。


我决定对此进行基准测试

1.设置函数

CREATE FUNCTION dbo.ExtractNumeric_TVF (@Input VARCHAR(8000))
RETURNS TABLE
AS
RETURN
(   WITH Numbers (Number) AS
    (   SELECT TOP (LEN(@Input)) ROW_NUMBER() OVER(ORDER BY N1.N)
        FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N1 (N)         -- 100
        CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N2 (N)   -- 100
        CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N3 (N)   -- 1,000
        CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N4 (N)   -- 10,000
    )
    SELECT  Stripped = x.data.value('.', 'VARCHAR(MAX)')
    FROM    (   SELECT  SUBSTRING(@Input, n.Number, 1)
                FROM    Numbers n
                WHERE   n.Number <= LEN(@Input)
                AND     SUBSTRING(@Input, n.Number, 1) LIKE '[0-9]'
                ORDER BY n.Number
                FOR XML PATH(''), TYPE
            ) x (data)
);
GO
create function dbo.ExtractNumeric_UDF(@s varchar(8000))
returns varchar(8000)
as
begin
    declare @out varchar(max) = ''
    declare @c char(1)
    while len(@s) > 0 begin
        set @c = left(@s,1)
        if @c like '[0123456789]' set @out += @c
        set @s = substring(@s, 2, len(@s) -1)
    end
    return @out
end
GO

2。创建第一组样本数据和日志表

CREATE TABLE dbo.T (Value VARCHAR(8000) NOT NULL);
INSERT dbo.T (Value)
SELECT  TOP 1000 LEFT(NEWID(), CEILING(RAND(CHECKSUM(NEWID())) * 36))
FROM    sys.all_objects a
CROSS JOIN sys.all_objects b;

CREATE TABLE dbo.TestLog (Fx VARCHAR(255), NumberOfRows INT, TimeStart DATETIME2(7), TimeEnd DATETIME2(7))

3.运行测试

GO
DECLARE @T TABLE (Val VARCHAR(8000));
INSERT dbo.TestLog (fx, NumberOfRows, TimeStart)
VALUES ('dbo.ExtractNumeric_UDF', 1000, SYSDATETIME());

INSERT @T (Val)
SELECT  dbo.ExtractNumeric_UDF(Value)
FROM    dbo.T;

UPDATE  dbo.TestLog
SET     TimeEnd = SYSDATETIME()
WHERE   TimeEnd IS NULL;

GO 100
DECLARE @T TABLE (Val VARCHAR(8000));
INSERT dbo.TestLog (fx, NumberOfRows, TimeStart)
VALUES ('dbo.ExtractNumeric_TVF', 1000, SYSDATETIME());

INSERT @T (Val)
SELECT  f.Stripped
FROM    dbo.T
        CROSS APPLY dbo.ExtractNumeric_TVF(Value) f;

UPDATE  dbo.TestLog
SET     TimeEnd = SYSDATETIME()
WHERE   TimeEnd IS NULL;

GO 100

4.获取结果

SELECT  Fx,
        NumberOfRows,
        RunTime = AVG(DATEDIFF(MILLISECOND, TimeStart, TimeEnd))
FROM    dbo.TestLog
GROUP BY fx, NumberOfRows;

我在 1,000 和 10,000 行中执行了以下操作(仅使用 NEWID(),因此最多只能使用 36 个字符),结果是:

Fx                          NumberOfRows    RunTime
--------------------------------------------------------
dbo.ExtractNumeric_TVF      1000            31
dbo.ExtractNumeric_UDF      1000            56
dbo.ExtractNumeric_TVF      10000           280
dbo.ExtractNumeric_UDF      10000           510

因此,TVF 的到来时间不到 UDF 的一半。

我想测试边缘情况,所以放置了 1,000 行较长的字符串(5,400 个字符)

TRUNCATE TABLE dbo.T;
INSERT dbo.T (Value)
SELECT  TOP 1000 
        REPLICATE(CONCAT(NEWID(), NEWID(), NEWID(), NEWID(), NEWID()), 30)
FROM    sys.all_objects a
CROSS JOIN sys.all_objects b;

这就是 TVF 发挥作用的地方,运行速度提高了 5 倍以上:

Fx                      NumberOfRows    RunTime 
------------------------------------------------
dbo.ExtractNumeric_TVF  1000            2485    
dbo.ExtractNumeric_UDF  1000            12955   

【讨论】:

  • 这就像一个魅力(而且速度也很快,1 秒内 52 000 条记录,我认为这需要更长的时间)。
  • 我将我的解决方案添加到您的测试中,虽然它也完全破坏了循环、标量函数方法,但它的性能绝对不如这个解决方案。
  • @GarethD 所以我试图通过将它复制到一个新的查询中来创建你的 UDF 并运行它。然后我使用 Select f.stripped from myDatabase t cross apply dbo.ExtractNumeric_TVF(t.articlenr) f 但这不起作用(无效的列名 articlenr)。有没有可能我可以得到一些帮助?我想在连接等上使用所有整数字符串,所以查询变得如此之大而不使用 UDF(我是新手)。
  • 列名无效的问题与函数无关,只是表示您的表中没有名为articlenr 的列。您能否将您的完整查询和表结构以及示例数据发布到一个新问题中,我会看看。
  • @GarethD 谢谢!我现在想通了。现在它甚至可以作为一个函数工作!
【解决方案2】:

我也真的不喜欢循环解决方案,所以我决定尝试一下。这是使用预定义的计数表,但与此处已发布的其他表非常相似。

这是我的计数表。我将此作为我系统上的一个视图。

create View [dbo].[cteTally] as

WITH
    E1(N) AS (select 1 from (values (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))dt(n)),
    E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
    E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
    cteTally(N) AS 
    (
        SELECT  ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
    )
select N from cteTally
GO

因为我不喜欢循环,所以我决定使用表值函数方法,这让我可以毫不费力地在其他查询中重用此功能。这是编写此类函数的一种方法。

create function GetOnlyNumbers
(
    @SearchVal varchar(8000)
) returns table as return

    with MyValues as
    (
        select substring(@SearchVal, N, 1) as number
            , t.N
        from cteTally t 
        where N <= len(@SearchVal)
            and substring(@SearchVal, N, 1) like '[0-9]'
    )

    select distinct NumValue = STUFF((select number + ''
                from MyValues mv2
                order by mv2.N
                for xml path('')), 1, 0, '')
    from MyValues mv

这看起来不错,但证据就在布丁中。让我们用我们的样本数据把它拿出来,然后踢几次。

DECLARE @Tabl as table
(
   dats nvarchar(15)
)

INSERT INTO @Tabl VALUES
('103-P705hh'),
('115-xxx-44'),
('103-705.13'),
('525-hheef4')


select *
from @Tabl t
cross apply dbo.GetOnlyNumbers(t.dats) x

当然看起来又漂亮又整洁。我针对此处发布的其他几个解决方案进行了测试,并且没有进行深入测试,这似乎比此时发布的其他方法要快得多。

【讨论】:

    【解决方案3】:
    DECLARE @Tabl as table
    (
       ID   INT,
       dats nvarchar(15)
    )
    
    INSERT INTO @Tabl VALUES
    (1, '103-P705hh'),
    (2, '115-xxx-44'),
    (3, '103-705.13'),
    (4, '525-hheef4')
    
    
    SELECT T.ID, t.dats
    ,(
      SELECT SUBSTRING(tt.dats,V.number,1)
      FROM @Tabl tt
        JOIN master.dbo.spt_values V ON V.type='P' AND V.number BETWEEN 1 AND LEN(tt.dats)
      WHERE tt.ID=T.ID AND SUBSTRING(TT.dats,V.number,1) LIKE '[0-9]'
      ORDER BY V.number
      FOR XML PATH('')
     ) S
    FROM @Tabl t
    ORDER BY T.ID;
    

    【讨论】:

      【解决方案4】:

      你可以使用 udf 吗?如果是这样,试试这个

      create alter function numerals(@s varchar(max))
      returns varchar(max)
      as
      begin
          declare @out varchar(max) = ''
          declare @c char(1)
          while len(@s) > 0 begin
              set @c = left(@s,1)
              if @c like '[0123456789]' set @out += @c
              set @s = substring(@s, 2, len(@s) -1)
          end
          return @out
      end
      

      在你的临时表上使用它...

      select dbo.numerals(dats) from @Tabl

      另一种解决方案,它不使用 UDF,但仅当您的表具有主键时才有效,它使用递归 CTE。它是:

      DECLARE @Tabl as table
      (pk int identity not null,  -- <=== added a primary key
       dats nvarchar(max) )
      
      INSERT INTO @Tabl VALUES
        ('103-P705hh'),
        ('115-xxx-44'),
        ('103-705.13'),
        ('525-hheef4');
      
       with newVals(pk, pos, newD) as 
           (select pk, 1, 
               case when left(Dats,1) like '[0123456789]'  
                    then left(Dats,1) else '' end
           from @tabl
           Union All
           Select t.pk, pos + 1, n.newD +
              case when substring(dats, pos+1, 1) like '[0123456789]'   
                   then substring(dats, pos+1, 1) else '' end          
           from @tabl t join newVals n on n.pk = t.pk
           where pos+1 <= len(dats) )         
           Select newD from newVals x         
           where pos = (Select Max(pos)
                        from newVals 
                        where pk = x.pk)
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2012-02-21
        • 2010-12-31
        相关资源
        最近更新 更多