使用PatIndex将字符串转换为整数

时间:2017-02-15 14:16:58

标签: sql-server string

我想从相当复杂的字符串返回整数,这些字符串将-.等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

我昨天有一个关于这个问题的主题,以涵盖多个-.存在的情况,但正如回程中所见,这实际上已经得到了解决。但是,扩展我使用的数据库时遇到了更复杂的字符串,例如我在这里提到的字符串。

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

此致 Cenderze

4 个答案:

答案 0 :(得分:4)

我已经看到大量的解决方案使用带有循环的标量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;

我做了以下(仅使用NEWID(),因此最多只有36个字符)超过1,000和10,000行,结果是:

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的一半。

我想测试边缘情况,所以要放1000行更长的字符串(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 :(得分: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

当然看起来很干净整洁。我对这里发布的其他几个解决方案进行了测试,没有进行深度测试,这似乎比目前发布的其他方法快得多。

答案 2 :(得分:1)

你能用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)

答案 3 :(得分:1)

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;