2013-07-02 141 views
12

我有一個包含帶佔位符的文本字段的表。事情是這樣的:用varchar(max)字段替換值加入

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的任何地方包含對10萬條記錄)

我需要找到最快的方法將這些佔位符從字符串連接到查找表並用值替換。所以,我的結果應該是這樣的(第1行):

This is some notes Dog this Cat, Cow. End.

我想出什麼樣的主意是每一行分割爲多個的每一個佔位符,然後加入它來查找表,然後CONCAT記錄回到原始行具有新的價值,但平均需要大約10-30秒。

+2

你可以發佈需要10-30秒的當前解決方案嗎? –

+0

你有沒有考慮過使用SQL CLR? – RBarryYoung

+0

@RBarryYoung,是的,我的服務器啓用了CLR,但我無法將表傳遞給CLR,AFAIK CLR不允許傳遞數據表。 – user194076

回答

4

我第二評論即TSQL只是不適合這種操作,但如果你必須在DB做在這裏是一個用函數來管理多個替代語句的例子。

既然你在每一個音符(5-15)和一個非常大的數字標記(1萬-10萬),我的功能首先從輸入作爲潛在令牌提取的令牌,並使用該組的數量相對較少的令牌加入你的查詢(下面的dbo.Token)。在每個音符中查找您的代幣的的任何都是非常麻煩的。

我做了一些使用50k標記和5k筆記的perf測試,這個功能運行得非常好,在<(在我的筆記本電腦上)完成了<。請回報此策略如何爲您執行。

注:在您的示例數據令牌格式不統一(##_#, ##_##, #_#),我猜測這只是一個錯字,並承擔所有標記取## 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. 
+0

在我的特殊情況下,您的解決方案效果最佳。非常感謝你的回答! – user194076

6

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.

+0

謝謝你的好腳本。對我的數據來說,這個運行真的很慢。甚至比分裂和加入還要慢。如果我不需要更新表格怎麼辦?我可以在單個選擇中做到這一點嗎?或者你知道寫一個使用正則表達式的CLR是否合理? – user194076

+1

增加了第二個解決方案,這是如何執行的?在我工作的地方不允許使用CLR,但我確信你可以在沒有正則表達式的情況下做一個簡單的替換。 – Andomar

+0

第二種方法是頭腦風暴!它適用於少量查找和大量Notes(我總共有1k個註釋),但只要嘗試所有查找(15k),執行操作需要幾分鐘的時間。我不確定是否有辦法優化這個。我注意到的一件事是從所有可用的15k中的一個查詢中只使用少量(5-20​​)查找字段。也許我可以以某種方式預掃音,並且只使用這些查找而不是所有15k迭代? – user194076

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

0

我真的不知道它將如何與查詢的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

+0

這是一個超慢建構查詢。無論如何,它會拋出一個錯誤: 您的SQL語句的某些部分嵌套太深。重寫查詢或將其分解爲更小的查詢。 – user194076

9

你可以嘗試拆分使用數字表的字符串,並將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

+0

尼斯腳本Mikael!這個比我的速度慢了一些(10s vs 2s),但是更加合理。先生,幹得好。 –

+0

@NathanSkerl謝謝。我沒有對此進行任何性能測試,但我猜'Lookup.Name'上的聚集鍵會最有幫助。我也認爲這是OP已經在做的事情。 *「每個佔位符將每行拆分爲多個,然後將其加入查找表,然後concat用新值記錄回原始行」*我添加了這一個,因爲它缺少了:)並且我也許做了一個更好的工作來拆分字符串。另外,使用'value'從XML讀取字符串比不使用'value'慢得多,但如果你不這樣做,你會遇到'<>&'的麻煩。 –

1

爲了獲得速度,可以預處理的音符模板成爲一個更有效的形式。這將是一系列片段,每個片段都以替代結束。最後一個片段的替換可能爲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 

預處理記模板成了碎片會使用其他職位的字符串分割技術是直接。

不幸的是,我不在我可以測試這個位置,但它應該工作正常。

0

現在對於一些遞歸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)