Oracle sql merge to insert and delete but not update - sql

Is there a way to use oracle merge to insert and delete but not update?
I have a table representing a set of values related to a single row in another table. I could change the set of values by deleting them all and adding back the new set, or by selectively deleting some and adding others, but I am interested in making it a single statement if possible.
Here is a working example with update. In order to make this work, I had to add dummy so that a column was available to update that was not in the on condition. Is there some way to only delete and insert without a dummy column to update?
No column from the on condition may be in the update set list even if it is not actually updated.
create table every_value ( the_value varchar2(32) );
create table paired_value ( the_id number, a_value varchar2(32) , dummy number default 0 );
-- the_id is a foreign_key to a row in another table
insert into every_value ( the_value ) values ( 'aaa' );
insert into every_value ( the_value ) values ( 'abc' );
insert into every_value ( the_value ) values ( 'ace' );
insert into every_value ( the_value ) values ( 'adg' );
insert into every_value ( the_value ) values ( 'aei' );
insert into every_value ( the_value ) values ( 'afk' );
-- pair ace and afk with id 3
merge into paired_value p using every_value e
on ( p.the_id = 3 and p.a_value = e.the_value )
when matched then update set dummy=dummy+1
delete where a_value not in ('ace','afk')
when not matched then insert (the_id,a_value)
values (3,e.the_value)
where e.the_value in ('ace','afk');
-- pair ace and aei with id 3
-- should remove afk, add aei, do nothing with ace
merge into paired_value p using every_value e
on ( p.the_id = 3 and p.a_value = e.the_value )
when matched then update set dummy = dummy+1
delete where a_value not in ('ace','aei')
when not matched then insert (the_id,a_value)
values (3,e.the_value)
where e.the_value in ('ace','aei');
-- pair aaa and adg with id 4
merge into paired_value p using every_value e
on ( p.the_id = 4 and p.a_value = e.the_value )
when matched then update set dummy = dummy+1
delete where a_value not in ('aaa','adg')
when not matched then insert (the_id,a_value)
values (4,e.the_value)
where e.the_value in ('aaa','adg');
select * from paired_value;
I have tried this in oracle 10g and, with this sqlfiddle, oracle 11g.

No, you cannot delete rows that have not been updated by the merge command.
Here is documentation: http://docs.oracle.com/cd/B28359_01/server.111/b28286/statements_9016.htm
Specify the DELETE where_clause to clean up data in a table while
populating or updating it. The only rows affected by this clause are
those rows in the destination table that are updated by the merge
operation. The DELETE WHERE condition evaluates the updated value, not
the original value that was evaluated by the UPDATE SET ... WHERE
condition. If a row of the destination table meets the DELETE
condition but is not included in the join defined by the ON clause,
then it is not deleted. Any delete triggers defined on the target
table will be activated for each row deletion.
That means, that rows must be updated. Hovewer, you don't need to update all rows, after UPDATE use the same WHERE clause as you are using after DELETE
when matched then update set dummy=dummy
where a_value not in ('ace','afk')
delete
where a_value not in ('ace','afk')

I have found you can set the column to itself:
MERGE ...
WHEN MATCHED THEN
UPDATE SET a_value = a_value WHERE a_value not in ('ace','afk')
DELETE WHERE a_value not in ('ace','afk')
This negates the need for the dummy column.

Related

Trigger to update foreign key field after insert on same table

I have two tables:
Table1 (surveyid [PKID], surveyname)
Table2 (visitid [PKID], surveyname, surveyid [FKID - refers to Table1]).
After inserting a new row into Table2, I would like to update Table2.surveyid with the surveyid from Table1, based on matching surveyname.
I thought it maybe wasn't possible (or good practice?) to create a trigger to update the same table. But I seem to have created a trigger that will do this. The problem is that after insert, the trigger updates the surveyid for every row, instead of just the newly inserted rows.
This trigger code works, but how do I ensure the trigger only updates the surveyid for newly inserted rows, and not all rows?
CREATE TRIGGER tr_update_table2_fk
ON Table2
AFTER INSERT
AS
BEGIN
UPDATE Table2
SET surveyid = (SELECT t1.surveyid
FROM Table1 t1
WHERE t1.surveyname = Table2.surveyname)
END;
Thank you MattM and DaleK, you've helped me figure out the answer. I was adding the inserted table into the subquery where clause before, instead of the query where clause. This does the trick:
CREATE TRIGGER tr_update_table2_fk
on Table2
AFTER INSERT
AS
BEGIN
UPDATE Table2 SET
surveyid = (
SELECT t1.surveyid
FROM Table1 t1
WHERE t1.surveyname = Table2.surveyname
)
WHERE Table2.visitid IN (SELECT visitid FROM inserted)
END;
Yes, the inserted table is the answer.
I'd use it to capture the visitids of the inserted rows, then filter by them in a WHERE clause on the end of your UPDATE statement in your trigger.
E.g.
CREATE OR ALTER TRIGGER tr_update_table2_fk
ON Table2
AFTER INSERT
AS
BEGIN
DROP TABLE IF EXISTS #VisitIds ;
CREATE TABLE #VisitIds ( [id] INT ) ;
INSERT INTO #VisitIds ( [id] )
SELECT [visitid]
FROM inserted ;
UPDATE Table2
SET [surveyid] =
(
SELECT t1.[surveyid]
FROM Table1 AS t1
WHERE t1.[surveyname] = Table2.[surveyname]
)
WHERE [visitid] IN ( SELECT [id] FROM #VisitIds ) ;
END
GO

How to bring both old and new vale in single row in oracle sql?

Create table query
CREATE TABLE ID_TAB (
ID VARCHAR2(20),
ID_VALUE VARCHAR2(20), FLAG VARCHAR2(20)
);
CREATE TABLE FACT_TABLE (
ID VARCHAR2(20),
VALUE VARCHAR2(20),
NAME VARCHAR2(100)
);
Insert Query
INSERT INTO ID_TAB VALUES('100','ABC','N');
INSERT INTO ID_TAB VALUES('120','ABC','Y');
INSERT INTO FACT_TABLE VALUES('100','MAX','ORANGE');
My objective is to update the fact table 'ID' column to 120 because it has the FLAG value as 'Y'
My original table has 50 million records.
How can we write the query in merge or using Update?
You could try the following (I didn't try it though):
UPDATE fact_table
SET fact_table.id = (SELECT yes_tab.id
FROM id_tab no_tab
JOIN id_tab yes_tab
ON no_tab.id_value = yes_tab.id_value
AND yes_tab.flag = 'Y'
WHERE no_tab.id = fact_table.id)
WHERE EXISTS (SELECT *
FROM id_tab
WHERE id_tab.id = fact_table.id
AND id_tab.flag = 'N');
The WHERE EXISTS makes sure you update only the elements with the flag in the corresponding ID_TAB set to 'N'.
The query for the SET searches the ID of the corresponding element having the flag set to 'Y'.
To get a performant solution (i.e. avoid the join completely) you may switch to a dynamic SQL.
I extended you example a bit to have more updated keys as follows.
Note that there is maximal one new value per ID_VALUE, but more old value with N flag are allowed, which all must be updated.
INSERT INTO ID_TAB VALUES('100','ABC','N'); -- old key
INSERT INTO ID_TAB VALUES('110','ABC','N'); -- old key
INSERT INTO ID_TAB VALUES('120','ABC','Y'); -- new key
INSERT INTO ID_TAB VALUES('200','EFG','N'); -- old key
INSERT INTO ID_TAB VALUES('210','EFG','N'); -- old key
INSERT INTO ID_TAB VALUES('220','EFG','Y'); -- new key
INSERT INTO FACT_TABLE VALUES('100','MAX','ORANGE');
INSERT INTO FACT_TABLE VALUES('110','MIN','ORANGE');
INSERT INTO FACT_TABLE VALUES('200','MAX','APPLE');
INSERT INTO FACT_TABLE VALUES('210','MIN','APPLE');
INSERT INTO FACT_TABLE VALUES('220','NEW','APPLE');
commit;
The UPDATE statements reflects all th eupdate options, e.g. the keys 100 and 110 should be chnaged to 200.
This leads to the following update
update FACT_TABLE
set ID = case
when ID in ('100','110') then '200'
when ID in ('200','210') then '220'
end
where ID in ('100','110','200','210');
Note that the where condition consist of all the keys with flag N and the case statement is produced per ID_VALUE mapping all keys with the flag N to the (only one) key with flag Y.
This makes the generation of the UPDATE statement an easy little task of a query on the table ID_TAB. See the query below. For the list creation the function LISTAGG is used. The main parts are commented in the query.
with old_keys as (
select /* get list of all old values */
listagg(''''||ID||'''',',') within group (order by ID) old_keys
from ID_TAB where flag = 'N'),
case_stmt as (
select ID_VALUE,
listagg(case when flag = 'N' then ''''||ID||'''' end,',') within group (order by ID) old_keys,
max(''''||case when flag = 'Y' then ID end||'''') new_key
from ID_TAB
group by ID_VALUE),
case_stmt2 as (
select
'when ID in ('||OLD_KEYS ||') then ' ||NEW_KEY ||' /* update for '|| ID_VALUE || ' */' case_when, ID_VALUE
from case_stmt),
case_stmt3 as ( /* concatenate CASE WHEN */
select listagg(CASE_WHEN,chr(13)) within group (order by ID_VALUE) case_when from case_stmt2)
select
'update FACT_TABLE
set ID = case
'||
case_when ||
'
end
where ID in ('||
(select old_keys from old_keys)||')' as update_stmt
from case_stmt3
On the sample data this SQL string is returned
update FACT_TABLE
set ID = case
when ID in ('100','110') then '120' /* update for ABC */
when ID in ('200','210') then '220' /* update for EFG */
end
where ID in ('100','110','200','210')

SQL CTE Syntax to DELETE / INSERT rows

What's the CTE syntax to delete from a table, then insert to the same table and return the values of the insert?
Operating on 2 hours of sleep and something doesn't look right (besides the fact that this won't execute):
WITH delete_rows AS (
DELETE FROM <some_table> WHERE id = <id_value>
RETURNING *
)
SELECT * FROM delete_rows
UNION
(
INSERT INTO <some_table> ( id, text_field )
VALUES ( <id_value>, '<text_field_value>' )
RETURNING *
)
The expected behavior is to first clear all the records for an ID, then insert records for the same ID (intentionally not an upsert) and return those inserted records (not the deletions).
Your question update made clear that you cannot do this in a single statement.
Packed into CTEs of the same statement, both operations (INSERT and DELETE) would see the same snapshot of the table and execute virtually at the same time. I.e., the INSERT would still see all rows that you thought to be deleted already. The manual:
All the statements are executed with the same snapshot (see Chapter 13), so they cannot "see" one another's effects on the target tables.
You can wrap them as two independent statements into the same transaction - which doesn't seem strictly necessary either, but it would allow the whole operation to succeed / fail atomically:
BEGIN;
DELETE FROM <some_table> WHERE id = <id_value>;
INSERT INTO <some_table> (id, text_field)
VALUES ( <id_value>, '<text_field_value>')
RETURNING *;
COMMIT;
Now, the INSERT can see the results of the DELETE.
CREATE TABLE test_table (value TEXT UNIQUE);
INSERT INTO test_table SELECT 'value 1';
INSERT INTO test_table SELECT 'value 2';
WITH delete_row AS (DELETE FROM test_table WHERE value='value 2' RETURNING 0)
INSERT INTO test_table
SELECT DISTINCT 'value 2'
FROM (SELECT 'dummy') dummy
LEFT OUTER JOIN delete_row ON TRUE
RETURNING *;
The query above handles the situations when DELETE deletes 0/1/some rows.
Elaborating on skif1979's "DelSert" CTE method, the "Logged DelSert:"
-- setups
DROP TABLE IF EXISTS _zx_t1 ;
CREATE TEMP TABLE
IF NOT EXISTS
_zx_t1
( id bigint
, fld2 bigint
, UNIQUE (id)
);
-- unique records
INSERT INTO _zx_t1 SELECT 1, 99;
INSERT INTO _zx_t1 SELECT 2, 98;
WITH
_cte_del_row AS
( DELETE
FROM _zx_t1
WHERE id = 2
RETURNING id as _b4_id, fld2 as _b4_fld2 -- returns complete deleted row
)
, _cte_delsert AS
( INSERT
INTO _zx_t1
SELECT DISTINCT
_cte_del_row._b4_id
, _cte_del_row._b4_fld2 + 1
from (SELECT null::integer AS _zunk) _zunk -- skif1979's trick here
LEFT OUTER JOIN _cte_del_row -- clever LOJ magic
ON TRUE -- LOJ cartesian product
RETURNING id as _aft_id , fld2 as _aft_fld2 -- return newly "delserted" rows
)
SELECT * -- returns before & after snapshots from CTE's
FROM
_cte_del_row
, _cte_delsert ;
RESULT:
_b4_id | _b4_fld2 | _aft_id | _aft_fld2
--------+----------+---------+-----------
2 | 209 | 2 | 210
AFAICT these all occur linearly w/in a unit of work, akin to a journaled or logged update.
Workable for
Child records
OR Schema w/ no FK
OR FK w/ cascading deletes
Not workable for
Parent records w/ FK & no cascading deletes
A related (& IMO better) answer, akin to the "Logged DelSert" is this, a logged "SelUp" :
-- setups
DROP TABLE IF EXISTS _zx_t1 ;
CREATE TEMP TABLE
IF NOT EXISTS
_zx_t1
( id bigint
, fld2 bigint
, UNIQUE (id)
);
-- unique records
INSERT INTO _zx_t1 SELECT 1, 99;
INSERT INTO _zx_t1 SELECT 2, 98;
WITH
_cte_sel_row AS
( SELECT -- start unit of work with read
id as _b4_id -- fields need to be aliased
,fld2 as _b4_fld2 -- to prevent ambiguous column errors
FROM _zx_t1
WHERE id = 2
FOR UPDATE
)
, _cte_sel_up_ret AS -- we're in the same UOW
( UPDATE _zx_t1 -- actual table
SET fld2 = _b4_fld2 + 1 -- some actual work
FROM _cte_sel_row
WHERE id = _b4_id
AND fld2 < _b4_fld2 + 1 -- gratuitous but illustrates the point
RETURNING id as _aft_id, fld2 as _aft_fld2
)
SELECT
_cte_sel_row._b4_id
,_cte_sel_row._b4_fld2 -- before
,_cte_sel_up_ret._aft_id
,_cte_sel_up_ret._aft_fld2 -- after
FROM _cte_sel_up_ret
INNER JOIN _cte_sel_row
ON TRUE AND _cte_sel_row._b4_id = _cte_sel_up_ret._aft_id
;
RESULT:
_b4_id | _b4_fld2 | _aft_id | _aft_fld2
--------+----------+---------+-----------
2 | 209 | 2 | 210
See also:
https://rob.conery.io/2018/08/13/transactional-data-operations-in-postgresql-using-common-table-expressions/

How to Delete the Existing data based on Where clause condition using Merge

i have written a merge Statement where i am facing trouble to delete the data basing on Where Clause Condition.
Let me explain my scenario Clearly
For example i have inserted Data from Source to Target based on Date Key Condition.Take an Instance 10 Records Inserted.
For example some changes in the records and it has been updated through the Merge Statement .
For the Same Date key based on Conditions now three records has came and need to be inserted and rest should be deleted for that Date Key.
How i need to proceed on this before 10 records are not getting deleted and new records adding for that one
My Example Code :
DELETE FROM #Table1
CREATE TABLE #Table1
(ID INT ,Name VARCHAR(30),DATEKEY INT)
INSERT INTO #Table1 (ID,Name,DATEKEY)VALUES (1,'Mohan',20131231)
INSERT INTO #Table1 (ID,Name,DATEKEY)VALUES (2,'Raj',20131231)
INSERT INTO #Table1 (ID,Name,DATEKEY)VALUES (3,'Majjaa',20131231)
INSERT INTO #Table1 (ID,Name,DATEKEY)VALUES (4,'Majjaa',20131231)
CREATE TABLE #Table2
(ID INT ,Name VARCHAR(30),DATEKEY INT)
DECLARE #i_DateKey INT
SET #i_DateKey = '20131231'
MERGE #Table2 AS T
USING (
SELECT pdp.ID
,pdp.Name
,pdp.DATEKEY
FROM #Table1 AS pdp
WHERE (
pdp.DateKey = #i_DateKey
OR #i_DateKey IS NULL
)
) AS S
ON T.ID = S.ID
AND T.DateKey = S.DateKey
AND T.NAME = S.NAME
WHEN MATCHED
THEN
UPDATE
SET T.NAME = S.NAME
WHEN NOT MATCHED BY TARGET
THEN
INSERT
VALUES (
S.ID
,S.Name
,S.DateKey
)
WHEN NOT MATCHED BY SOURCE
THEN
DELETE ;
Now the target table will be loaded with Rows now if i send the another row for Same Date key then it need to be deleted all the 4 rows and reload the new Row if the new row is same then need to update
i think this is one big mistake,
ON T.ID = S.ID --i am not sure about this
AND T.DateKey = S.DateKey
AND T.NAME = S.NAME -- remove this because this when matched then update will do what ?
WHEN MATCHED
THEN
UPDATE
SET T.NAME = S.NAME

How to know that MERGE operation was INSERT or UPDATE?

Let say I have,
MERGE INTO SHARE_AD_GROUP A
USING (
SELECT SHARE_AD_GROUP_ID,
SHARE_ID,
AD_GROUP,
SHARE_PERMISSIONS
FROM SHARE_AD_GROUP
WHERE SHARE_ID = #shareID AND AD_GROUP = #ownerId
) B ON (A.SHARE_AD_GROUP_ID = B.SHARE_AD_GROUP_ID)
WHEN MATCHED THEN
UPDATE SET A.SHARE_PERMISSIONS = B.SHARE_PERMISSIONS
WHEN NOT MATCHED THEN
INSERT (SHARE_PERMISSIONS) VALUES(#sharePermissions);
-- In Here how do I know that it is insert or update
How to know that MERGE operation was INSERT or UPDATE after INSERT OR UPDATE?
Please refer here
DECLARE #SummaryOfChanges TABLE(Change VARCHAR(20));
MERGE tblTarget AS Target
USING (SELECT Col1,Col2 FROM tblSource)
AS Source
ON (Target.Col1 = Source.Col1)
WHEN MATCHED THEN
UPDATE SET target.Col2 = source.Col2 -- Need to get affected rows here
WHEN NOT MATCHED BY TARGET THEN
INSERT (Col1,Col2) VALUES (Col1,Col2); -- Need to get affected rows here
OUTPUT $action INTO #SummaryOfChanges;
SELECT Change, COUNT(*) AS CountPerChange
FROM #SummaryOfChanges
GROUP BY Change;