Home > Software engineering >  Update a row multiple times when performing a join
Update a row multiple times when performing a join

Time:07-13

CREATE TABLE Replacements 
(
    OldVal nvarchar(max),
    NewVal nvarchar(max)
);

CREATE TABLE Foo
(
    Val nvarchar(max)
);

insert into Replacements values ('old1','new1');
insert into Replacements values ('old2','new2');
insert into Replacements values ('old3','new3');
insert into Replacements values ('old4','new4');

insert into Foo values ('old1');
insert into Foo values ('old3');
insert into Foo values ('old2;old4');

I have some data that may be delimited by semicolon. I need to join the data against a lookup table and replace the old data with the new data. This works fine when the data is not delimited, but if it is delimited, it only performs the first update.

update f
set f.val = Replace(f.val, r.OldVal, r.NewVal)
from Foo f 
inner join replacements r on (CHARINDEX(r.OldVal, f.Val) > 0);

select * from Foo;
Val
new1
new3
new2;old4

How can I perform multiple updates on the same row? Is there a better way for finding/replacing strings within delimited strings? Compatibility will need to be back to SQL Sever 2014.

http://sqlfiddle.com/#!18/c320d13/9

CodePudding user response:

In SQL Server, UPDATE statements can only affect 1 of each of the rows in the target table in any single statement.

This means that the expected solution is to execute this update multiple times, one hack for this is to simply script the update a fixed number of times:

update f
set f.val = Replace(f.val,r.OldVal,r.NewVal)
from
 Foo f inner join replacements r 
on 
(CHARINDEX(r.OldVal,f.Val) > 0);

update f
set f.val = Replace(f.val,r.OldVal,r.NewVal)
from
 Foo f inner join replacements r 
on 
(CHARINDEX(r.OldVal,f.Val) > 0);

http://sqlfiddle.com/#!18/c320d13/10

An alternative solution would be to recursively apply the update:

WHILE EXISTS (SELECT 1
    from Foo f 
    inner join replacements r on (f.Val = r.OldVal or CHARINDEX(r.OldVal,f.Val) > 0))
BEGIN
    update f
    set f.val = Replace(f.val,r.OldVal,r.NewVal)
    from
     Foo f inner join replacements r 
    on 
    (CHARINDEX(r.OldVal,f.Val) > 0);
END

**NOTE: **
This answer is based on the original post that specified the delimiter was unknown. With a known delimiter the UPDATE statement needs to be more specific, but ultimately the update will need to be applied multiple times.

Other possible solutions include using a CURSOR or splitting the concatenated field, replacing the tokens then re-joining the tokens back into a delimited string.


The docs are a little bit vague on why we need to do this but, if your UPDATE has multiple matches on the target table, only 1 of the matches will be applied, but there is no guarantee which one, it is indeterminate:

UPDATE: Best Practices
Use caution when specifying the FROM clause to provide the criteria for the update operation. The results of an UPDATE statement are undefined if the statement includes a FROM clause that is not specified in such a way that only one value is available for each column occurrence that is updated, that is if the UPDATE statement is not deterministic. For example, in the UPDATE statement in the following script, both rows in Table1 meet the qualifications of the FROM clause in the UPDATE statement; but it is undefined which row from Table1 is used to update the row in Table2.

This has been discussed before on SO: SQL update same row multiple times and is explicitly disallowed in the MERGE statement, it would result in this error:

The MERGE statement attempted to UPDATE or DELETE the same row more than once.
This happens when a target row matches more than one source row. 
A MERGE statement cannot UPDATE/DELETE the same row of the target table multiple times.
Refine the ON clause to ensure a target row matches at most one source row, or use the GROUP BY clause to group the source rows.
  • Related