【问题标题】:replace value in varchar(max) field with join用 join 替换 varchar(max) 字段中的值
【发布时间】:2013-07-02 22:07:36
【问题描述】:

我有一个表格,其中包含带有占位符的文本字段。像这样的:

Row Notes  
1.  This is some notes ##placeholder130## this ##myPlaceholder##, #oneMore#. End.
2.  Second row...just a ##test#.   

(此表平均包含大约 1-5k 行。一行中占位符的平均数量为 5-15)。

现在,我有一个如下所示的查找表:

Name             Value
placeholder130    Dog
myPlaceholder     Cat
oneMore           Cow
test              Horse   

(查找表将包含 10k 到 100k 条记录)

我需要找到将这些占位符从字符串连接到查找表并替换为值的最快方法。所以,我的结果应该是这样的(第一行):

这是一些笔记 Dog this Cat, Cow。结束。

我想出的是为每个占位符将每一行拆分为多个,然后将其加入查找表,然后将记录连接回具有新值的原始行,但平均需要大约 10-30 秒。

【问题讨论】:

  • 你能发布当前需要 10-30 秒的解决方案吗?
  • 你考虑过使用 SQL CLR 吗?
  • @RBarryYoung,是的,我的服务器启用了 CLR,但我无法将表传递给 CLR,AFAIK CLR 不允许传递数据表。
  • 什么版本的 SQL Server?我赞同 Nathan Skerts 的评论:查看当前代码确实会大有帮助。
  • 有了这么大的查找表,你真的需要把它变成永久的,而不是临时的,并在 Name 上给它一个聚集索引。

标签: sql sql-server sql-server-2008-r2


【解决方案1】:

您可以尝试使用数字表拆分字符串并使用for xml path 重新构建它。

select (
       select coalesce(L.Value, T.Value)
       from Numbers as N
         cross apply (select substring(Notes.notes, N.Number, charindex('##', Notes.notes + '##', N.Number) - N.Number)) as T(Value)
         left outer join Lookup as L
           on L.Name = T.Value
       where N.Number <= len(notes) and
             substring('##' + notes, Number, 2) = '##'
       order by N.Number
       for xml path(''), type
       ).value('text()[1]', 'varchar(max)')
from Notes

SQL Fiddle

我借用this blog post by Aaron Bertrand的字符串拆分

【讨论】:

  • 不错的脚本米凯尔!这个运行速度比我的慢一点(10s vs 2s),但更简洁。干得好,先生。
  • @NathanSkerl 谢谢。我没有对此进行任何性能测试,但我猜Lookup.Name 上的集群键将是最有帮助的。我也认为这是 OP 已经在做的事情。 “为每个占位符将每一行拆分为多个,然后将其加入查找表,然后将记录连接回具有新值的原始行”我添加了这一行,因为它丢失了 :) 我也许做了一个更好地拆分字符串。此外,使用 value 从 XML 中获取字符串比不使用 value 慢得多,但如果不使用 &lt;&gt;&amp;,则会遇到麻烦。
【解决方案2】:

SQL Server 处理字符串的速度不是很快,因此这可能最好在客户端完成。让客户端加载整个查找表,并在注释到达时替换它们。

话虽如此,当然可以用SQL来完成。这是一个递归 CTE 的解决方案。每个递归步骤执行一次查找:

; with  Repl as
        (
        select  row_number() over (order by l.name) rn
        ,       Name
        ,       Value
        from    Lookup l
        )
,       Recurse as
        (
        select  Notes
        ,       0 as rn
        from    Notes
        union all
        select  replace(Notes, '##' + l.name + '##', l.value)
        ,       r.rn + 1
        from    Recurse r
        join    Repl l
        on      l.rn = r.rn + 1
        )
select  *
from    Recurse
where   rn = 
        (
        select  count(*)
        from    Lookup
        )
option  (maxrecursion 0)

Example at SQL Fiddle.

另一个选项是 while 循环,以不断替换查找,直到找不到更多:

declare @notes table (notes varchar(max))

insert  @notes
select  Notes
from    Notes

while 1=1
    begin

    update  n
    set     Notes = replace(n.Notes, '##' + l.name + '##', l.value)
    from    @notes n
    outer apply
            (
            select  top 1 Name
            ,       Value
            from    Lookup l
            where   n.Notes like '%##' + l.name + '##%'
            ) l
    where   l.name is not null

    if @@rowcount = 0
        break
    end   

select  *
from    @notes

Example at SQL Fiddle.

【讨论】:

  • 感谢您提供的精彩脚本。对于我的一组数据,这个运行速度真的很慢。甚至比拆分和加入还要慢。如果我不需要更新表怎么办?我可以一次选择吗?或者您知道编写使用正则表达式的 CLR 是否有意义?
  • 添加了第二个解决方案,效果如何?我工作的地方不允许使用 CLR,但我很肯定你可以在没有正则表达式的情况下进行简单的替换。
  • 第二种方法令人兴奋!它适用于少量查找和大量笔记(我总共有 1k 条笔记),但一旦我尝试所有查找(15k),就需要几分钟才能执行。我不确定是否有办法优化它。我注意到的一件事是,在所有 15k 可用的查询中,只有少数 (5-20) 个查找字段用于一个查询。也许我可以以某种方式预扫描笔记并且只使用那些查找而不是所有 15k 迭代?
  • 为什么 OUTER APPLY + WHERE IS NOT NULL?用 CROSS APPLY 替换它不会做同样的事情吗?
  • It appears SQL Server 足够聪明,可以识别出两者是一回事。 (为 CROSS APPLY 版本生成的计划与您的查询相同。)所以这可能是品味和/或清晰度的问题(无论如何都是主观的)。
【解决方案3】:

我赞同 tsql 不适合此操作的评论,但如果您必须在 db 中执行此操作,这里是使用函数管理多个替换语句的示例。

由于每个音符 (5-15) 中的标记数量相对较少且标记数量非常多 (10k-100k),因此我的函数首先从输入中提取标记作为潜在标记并使用该集合加入您的查找(下面的 dbo.Token)。要在每个笔记中查找 任何 标记的出现,工作量太大了。

我使用 50k 令牌和 5k 笔记进行了一些性能测试,这个功能运行得非常好,在

注意:在您的示例数据中,令牌格式不一致 (##_#, ##_##, #_#),我猜这只是一个错字,并假设所有令牌都采用 ##TokenName## 的形式。

--setup
    if object_id('dbo.[Lookup]') is not null
        drop table dbo.[Lookup];
    go
    if object_id('dbo.fn_ReplaceLookups') is not null
        drop function dbo.fn_ReplaceLookups;
    go

    create table dbo.[Lookup] (LookupName varchar(100) primary key, LookupValue varchar(100));
    insert into dbo.[Lookup]
        select '##placeholder130##','Dog' union all
        select '##myPlaceholder##','Cat' union all
        select '##oneMore##','Cow' union all
        select '##test##','Horse';
    go

    create function [dbo].[fn_ReplaceLookups](@input varchar(max))
    returns varchar(max)
    as
    begin

        declare @xml xml;
        select @xml = cast(('<r><i>'+replace(@input,'##' ,'</i><i>')+'</i></r>') as xml);

        --extract the potential tokens
        declare @LookupsInString table (LookupName varchar(100) primary key);
        insert into @LookupsInString
            select  distinct '##'+v+'##'
            from    (   select  [v] = r.n.value('(./text())[1]', 'varchar(100)'),
                                [r] = row_number() over (order by n)
                        from    @xml.nodes('r/i') r(n)
                    )d(v,r)
            where   r%2=0;

        --tokenize the input
        select  @input = replace(@input, l.LookupName, l.LookupValue)
        from    dbo.[Lookup] l
        join    @LookupsInString lis on 
                l.LookupName = lis.LookupName;

        return @input;
    end
    go          
    return            

--usage
    declare @Notes table ([Id] int primary key, notes varchar(100));
    insert into @Notes
        select 1, 'This is some notes ##placeholder130## this ##myPlaceholder##, ##oneMore##. End.' union all
        select 2, 'Second row...just a ##test##.';

    select  *,
            dbo.fn_ReplaceLookups(notes)
    from    @Notes;

返回:

Tokenized
--------------------------------------------------------
This is some notes Dog this Cat, Cow. End.
Second row...just a Horse.

【讨论】:

  • 在我的特殊情况下,您的解决方案效果最好。非常感谢您的回答!
【解决方案4】:

试试这个

;WITH CTE (org, calc, [Notes], [level]) AS
(
    SELECT [Notes], [Notes], CONVERT(varchar(MAX),[Notes]), 0 FROM PlaceholderTable

    UNION ALL

    SELECT  CTE.org, CTE.[Notes],
        CONVERT(varchar(MAX), REPLACE(CTE.[Notes],'##' + T.[Name] + '##', T.[Value])), CTE.[level] + 1
    FROM    CTE
    INNER JOIN LookupTable T ON CTE.[Notes] LIKE '%##' + T.[Name] + '##%'

)

SELECT DISTINCT org, [Notes], level FROM CTE
WHERE [level] = (SELECT MAX(level) FROM CTE c WHERE CTE.org = c.org)

SQL FIDDLE DEMO

查看下面的 devioblog 帖子以供参考

devioblog post

【讨论】:

    【解决方案5】:

    为了加快速度,您可以将笔记模板预处理为更有效的形式。这将是一个片段序列,每个片段都以替换结尾。最后一个片段的替换可能为 NULL。

    Notes
    Id     FragSeq    Text                    SubsId
    1      1          'This is some notes '   1
    1      2          ' this '                2
    1      3          ', '                    3
    1      4          '. End.'                null
    2      1          'Second row...just a '  4
    2      2          '.'                     null
    
    Subs
    Id  Name               Value
    1   'placeholder130'   'Dog'
    2   'myPlaceholder'    'Cat'
    3   'oneMore'          'Cow'
    4   'test'             'Horse'  
    

    现在我们可以通过简单的连接来进行替换。

    SELECT Notes.Text + COALESCE(Subs.Value, '') 
    FROM Notes LEFT JOIN Subs 
    ON SubsId = Subs.Id WHERE Notes.Id = ?
    ORDER BY FragSeq
    

    这会生成一个替换完成的片段列表。我不是 MSQL 用户,但在大多数 SQL 方言中,您可以很容易地将这些片段连接到一个变量中:

    DECLARE @Note VARCHAR(8000)
    SELECT @Note = COALESCE(@Note, '') + Notes.Text + COALSCE(Subs.Value, '') 
    FROM Notes LEFT JOIN Subs 
    ON SubsId = Subs.Id WHERE Notes.Id = ?
    ORDER BY FragSeq
    

    使用其他帖子的字符串拆分技术将笔记模板预处理为片段将很简单。

    不幸的是,我不在可以测试它的位置,但它应该可以正常工作。

    【讨论】:

      【解决方案6】:

      我真的不知道它会在 10k+ 的查找中表现如何。 旧的动态 SQL 表现如何?

      DECLARE @sqlCommand  NVARCHAR(MAX)
      SELECT @sqlCommand  = N'PlaceholderTable.[Notes]'
      
      SELECT @sqlCommand  = 'REPLACE( ' + @sqlCommand  + 
                            ', ''##' + LookupTable.[Name] + '##'', ''' + 
                            LookupTable.[Value] + ''')'  
      FROM LookupTable
      
      SELECT @sqlCommand  = 'SELECT *, ' + @sqlCommand  + ' FROM PlaceholderTable'
      
      EXECUTE sp_executesql @sqlCommand
      

      Fiddle demo

      【讨论】:

      • 这个构造查询超级慢。无论如何,它会引发错误:您的 SQL 语句的某些部分嵌套得太深。重写查询或将其分解为更小的查询。
      【解决方案7】:

      现在是一些递归 CTE。

      如果您的索引设置正确,则该索引应该非常快非常慢。在 r-CTE 方面,SQL Server 的极端性能总是让我感到惊讶...

      ;WITH T AS (
        SELECT
          Row,
          StartIdx = 1,                                  -- 1 as first starting index
          EndIdx = CAST(patindex('%##%', Notes) as int), -- first ending index
          Result = substring(Notes, 1, patindex('%##%', Notes) - 1)
                                                         -- (first) temp result bounded by indexes
        FROM PlaceholderTable -- **this is your source table**
        UNION ALL
        SELECT
          pt.Row,
          StartIdx = newstartidx,                        -- starting index (calculated in calc1)
          EndIdx = EndIdx + CAST(newendidx as int) + 1,  -- ending index (calculated in calc4 + total offset)
          Result = Result + CAST(ISNULL(newtokensub, newtoken) as nvarchar(max))
                                                         -- temp result taken from subquery or original
        FROM 
          T
          JOIN PlaceholderTable pt -- **this is your source table**
            ON pt.Row = T.Row
          CROSS APPLY(
            SELECT newstartidx = EndIdx + 2              -- new starting index moved by 2 from last end ('##')
          ) calc1
          CROSS APPLY(
            SELECT newtxt = substring(pt.Notes, newstartidx, len(pt.Notes))
                                                         -- current piece of txt we work on
          ) calc2
          CROSS APPLY(
            SELECT patidx = patindex('%##%', newtxt)     -- current index of '##'
          ) calc3
          CROSS APPLY(
            SELECT newendidx = CASE 
              WHEN patidx = 0 THEN len(newtxt) + 1
              ELSE patidx END                            -- if last piece of txt, end with its length
          ) calc4
          CROSS APPLY(
            SELECT newtoken = substring(pt.Notes, newstartidx, newendidx - 1)
                                                         -- get the new token
          ) calc5
          OUTER APPLY(
            SELECT newtokensub = Value
            FROM LookupTable
            WHERE Name = newtoken                        -- substitute the token if you can find it in **your lookup table**
          ) calc6
        WHERE newstartidx + len(newtxt) - 1  <= len(pt.Notes)  
                                                         -- do this while {new starting index} + {length of txt we work on} exceeds total length
      ) 
      ,lastProcessed AS (
        SELECT 
          Row, 
          Result,
          rn = row_number() over(partition by Row order by StartIdx desc)
        FROM T 
      )                                                  -- enumerate all (including intermediate) results
      SELECT *
      FROM lastProcessed
      WHERE rn = 1                                       -- filter out intermediate results (display only last ones)
      

      【讨论】:

        猜你喜欢
        • 2015-04-06
        • 1970-01-01
        • 2011-09-15
        • 1970-01-01
        • 1970-01-01
        • 2020-08-30
        • 1970-01-01
        • 2011-02-16
        • 2022-01-06
        相关资源
        最近更新 更多