Update a row multiple times when performing a join - sql

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

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

How to write a SQL update statement that will fail if it updates more than one row?

A common mistake when writing update statements is to forget the where clause, or to write it incorrectly, so that more rows than expected get updated. Is there a way to specify in the update statement itself that it should only update one row (and to fail if it would update more)?
Correcting an error in the number of rows updated requires thinking ahead - using a transaction, formatting it as a select first to check the number of rows - and then actually catching the error. It would be useful to be able to write in one place the expectation for the number of rows.
Combining a few facts, I found a working solution for Postgres.
A select will fail when comparing using = to a subquery that returns more than one row. (where x = (select ...))
Values can be returned from an update statement, using the returning clause. An update cannot be used as a subquery, but it can be used as a CTE, which can be used in a subquery.
Example:
create table foo (id int not null primary key, x int not null);
insert into foo (id, x) values (1,5), (2,5);
with updated as (update foo set x = 4 where x = 5 returning id)
select id from foo where id = (select id from updated);
The query containing the update fails with ERROR: more than one row returned by a subquery used as an expression, and the updates are not applied. If the update's where clause is adjusted to only match one row, the update succeeds.

Is Oracle MERGE NOT MATCHED THEN UPDATE possible?

We'd like to set the IS_DEL bit = 1 when a record exists in a Target table that doesn't exist in the Source table.
Is it possible to use a MERGE statement using the WHEN NOT MATCHED clause, but make it perform an UPDATE?
When attempting to do so, I'm getting a "ORA-00905: missing keyword" message.
MERGE
INTO AMEPSA.ENTERPRISE_LOCATION trg
USING (
SELECT C.LOCATION_KEY as LOCATION_KEY
FROM AMEPSA.ENTERPRISE_LOCATION C
INNER JOIN AMESTAGE.VW_LOCATION L ON C.REC_SRC_KEY_CD = L.LOCATION_ID
WHERE C.CURR_REC_IND = 'Y'
) src
ON (trg.LOCATION_KEY = src.LOCATION_KEY)
WHEN NOT MATCHED THEN UPDATE
SET trg.IS_DEL = 1
Does the "WHEN NOT MATCH" clause only support "THEN INSERT"?
From the documentation:
Use the MERGE statement to select rows from one or more sources for update or insertion into a table or view. You can specify conditions to determine whether to update or insert into the target table or view.
The syntax looks for rows in the source table (src) which do or do not have matching rows in the target table (trg). If there is a matching target row then it updates that; if there is not a matching row then it inserts a new row in the target table.
It does not, and cannot, look for rows in the target table that are not matched in the source table - which is what you are trying to identify and update.
The syntax diagrams for WHEN MATCHED and WHEN NOT MATCHED also make it clear that you cannot do WHEN NOT MATCHED THEN UPDATE.
Yes you can only insert when not match. See exact options in oracle merge.
The condition can refer to either the data source or the target table. If the condition is not true, then the database skips the update operation when merging the row into the table.

UPDATE/INSERT statement within an IF statement within a LOOP using sequence

CURSOR text IS
SELECT *
FROM DATA
CURSOR MATCHES IS
SELECT NAME
FROM DATA
INTERSECT
SELECT DESCRIPTION
FROM my_table
BEGIN
FOR i IN text
OPEN MATCHES
FETCH MATCHES INTO MATCH
CLOSE MATCH
IF i IN MATCH
THEN
UPDATE my_table
SET col1 = correlating_new_column1, col2 = correlating_new_column2, col3 = correlating_new_column3
WHERE table_im_trying_to_populate.code = my_seq.curval
ELSE
INSERT INTO TABLE_IM_TRYING_TO_POPULATE(CODE, NAME, DESCRIPTION, col1, col2, col3)
VALUES(my_seq.nextval, other_name, other_description, correlating_new_column1, correlating_new_column2, correlating_new_column3)
END IF;
END LOOP;
Basically I am trying to take an explicit cursor I made that is a select statement of a table and then do a line by line loop of that and put it into my other existing table. If it comes across a name in my other exisiting table it will update some of the columns. Else it inserts the whole record into that table. I am attempting to use sequence to update the 'code' column so that it updates where the code from the other existing table = my_seq.curval. Then for the inset it just goes to the next val. I know this is complicated but Im really just trying to see if I have the setup correct. Just started using sql developer for oracle not to long ago.
There are a huge number of problems with your code. However, it looks like what you're trying to do can be achieved with a single MERGE statement, along the lines of:
merge into table_im_trying_to_populate tgt
using data_table src
on (tgt.name = src.other_name
and tgt.description = src.other_description)
when matched then
update set tgt.col1 = src.correlating_new_column1,
tgt.col2 = src.correlating_new_column2,
tgt.col3 = src.correlating_new_column3
when not matched then
insert (tgt.code, tgt.name, tgt.description, tgt.col1, tgt.col2, tgt.col3)
values (my_seq.nextval, src.other_name, src.other_description, src.correlating_new_column1, src.correlating_new_column2, src.correlating_new_column3);
This assumes that the other_name and other_description columns in the data table is unique. I've also had to guess at what the join condition should be, since the join condition you had in your example update statement (table_im_trying_to_populate.code = my_seq.currval) didn't make any sense - you don't use currval to join against as a general rule, since it isn't populated unless you've previously pulled a value from the sequence in the same session.
If this doesn't match what you're trying to do, please update your question with some sample data in both tables and the expected output, and we should be able to help you further.

Postgres UPDATE..FROM query with multiple updates on the same row

I am attempting to optimise a bulk UPDATE statement in Postgres using the UPDATE..FROM syntax to update from a list of values. It works except when the same row might be updated more than once in the same query.
For example say I have a table "test" with columns "key" and "value".
update test as t set value = v.value from (values
('key1', 'update1'),
('key1', 'update2') )
as v (key, value)
where t.key = v.key;
My desired behavior is for the row with key 'key1' to be updated twice, finishing with value set to 'update2'. In practice sometimes the value is updated to update1 and sometimes to update2. Also an update trigger function on the table is only invoked once.
The documentation (http://www.postgresql.org/docs/9.1/static/sql-update.html) explains why:
When a FROM clause is present, what essentially happens is that the target table is joined to the tables mentioned in the from_list, and each output row of the join represents an update operation for the target table. When using FROM you should ensure that the join produces at most one output row for each row to be modified. In other words, a target row shouldn't join to more than one row from the other table(s). If it does, then only one of the join rows will be used to update the target row, but which one will be used is not readily predictable.
Because of this indeterminacy, referencing other tables only within sub-selects is safer, though often harder to read and slower than using a join.
Is there any way to reformulate this query to achieve the behavior I'm looking for? Does the reference to sub-selects in the documentation give a hint?
Example (assuming id is a PK in the target table, and {id, date_modified} is a PK in the source table)
UPDATE target dst
Set a = src.a , b = src.b
FROM source src
WHERE src.id = dst.id
AND NOT EXISTS (
SELECT *
FROM source nx
WHERE nx.id = src.id
-- use an extra key field AS tie-breaker
AND nx.date_modified > src.date_modified
);
(in fact, this is deduplication of the source table -> forcing the source table to the same PK as the target table)

OUTPUT Clause in Sql Server (Transact-SQL)

I Know that OUTPUT Clause can be used in INSERT, UPDATE, DELETE, or MERGE statement. The results of an OUTPUT clause in a INSERT, UPDATE, DELETE, or MERGE statements can be stored into a target table.
But, when i run this query
select * from <Tablename> output
I didn't get any error. The query executed as like select * from tablename with out any error and with same no. of rows
So what is the exact use of output clause in select statement. If any then how it can be used?
I searched for the answer but i couldn't find a answer!!
The query in your question is in the same category of errors as the following (that I have also seen on this site)
SELECT *
FROM T1 NOLOCK
SELECT *
FROM T1
LOOP JOIN T2
ON X = Y
The first one just ends up aliasing T1 AS NOLOCK. The correct syntax for the hint would be (NOLOCK) or ideally WITH(NOLOCK).
The second one aliases T1 AS LOOP. To request a nested loops join the syntax would need to be INNER LOOP JOIN
Similarly in your question it just ends up applying the table alias of OUTPUT to your table.
None of OUTPUT, LOOP, NOLOCK are actually reversed keywords in TSQL so it is valid to use them as a table alias without needing to quote them, e.g. in square brackets.
OUTPUT clause return information about the rows affected by a statement. OUTPUT Clause is used along with INSERT, UPDATE, DELETE, or MERGE statements as you mentioned. The reason it is used is because these statements themselves just return the number of rows effected not the rows effected. Thus the usage of OUTPUT with INSERT, UPDATE, DELETE, or MERGE statements helps the user by returning actual rows effected.
SELECT statement itself returns the rows and SELECT doesn't effect any rows. Thus the usage of OUTPUT clause with SELECT is not required or supported. If you want to store the results of a SELECT statement into a target table use SELECT INTO or the standard INSERT along with the SELECT statement.
EDIT
I guess I misunderstood your question. AS #Martin Smith mentioned its is acting an alias in the SELECT statement you mentioned.
IF OBJECT_ID('tempdelete') IS NOT NULL DROP TABLE tempdelete
GO
IF OBJECT_ID('tempdb..#asd') IS NOT NULL DROP TABLE #asd
GO
CREATE TABLE tempdelete (
name NVARCHAR(100)
)
INSERT INTO tempdelete VALUES ('a'),('b'),('c')
--Creating empty temp table with the same columns as tempdelete
SELECT * INTO #asd FROM tempdelete WHERE 1 = 0
DELETE FROM tempdelete
OUTPUT deleted.* INTO #asd
SELECT * FROM #asd
This is how you can put all the deleted records in to a table. The problem with that is that you have to define the table with all the columns matching the table from which you are deleting. This is how i do it.