Reputation: 75
How can one correctly archive data without "lost update"?
Here is what I want to do:
INSERT INTO
SELECT FOR UPDATE
DELETE SELETED rows.
But the syntax FOR UPDATE
is not supported in INSERT INTO ... SELECT...
Can one solve the problem using just SQL without Cursor on PL/SQL?
Example
create table authority(id number, key varchar2(128));
create table authority_arch(id number, key varchar2(128));
insert into authority(1, 'random_key1');
insert into authority(1, 'random_key2');
insert into authority(1, 'random_key3');
insert into authority(2, 'random_key4');
insert into authority(2, 'random_key5');
commit;
1 session
insert into authority_arch
select * from authority where id=2;
-- in this moment 2 session make insert! 'Lose rows' in next delete
delete from authority where id=2;
2 session
insert into authority(2, 'random_key6', sysdate+1);
commit;
in result have:
select * from authority
id | key
-----------
1 | random_key1
1 | random_key2
1 | random_key3
but i want delete ONLY selected rows
id | key
-----------
1 | random_key1
1 | random_key2
1 | random_key3
2 | random_key6
As a solution i use:
for rec in (select rowid as rid, a.* from authority a where id=2 FOR UPDATE nowait) loop
insert into authority_arch values(rec.id, rec.key);
delete from authority where rowid=rec.rid;
end loop;
Upvotes: 3
Views: 235
Reputation: 9865
The for update
clause doesn't help you here. This only locks the rows you query, stopping others updating/deleting them. Any new rows added are not locked! So the delete will always process the new rows.
To overcome this, there's a couple of basic approaches you can take:
You can do the first using flashback query. Get the database's SCN with dbms_flashback.get_system_change_number. Then use "as of scn" to get the rows as they existed at this time:
declare
insert_time pls_integer;
begin
insert_time := dbms_flashback.get_system_change_number;
insert into authority_arch
select * from authority as of scn insert_time
where id = 2;
dbms_lock.sleep(10); -- wait to allow insert in session 2
delete authority
where ( id, key ) in (
select id, key from authority as of scn insert_time
where id = 2
);
end;
/
You can use a temporary table for the second method. Or you could use bulk collection to fetch them into an array. And forall to insert+delete:
declare
type auth_rec is table of authority%rowtype index by binary_integer;
arch_recs auth_rec ;
begin
select *
bulk collect into arch_recs
from authority
where id = 2;
forall i in arch_recs.first .. arch_recs.last
insert into authority_arch values arch_recs(i);
dbms_lock.sleep(10); -- wait to allow insert in session 2
forall i in arch_recs.first .. arch_recs.last
delete authority
where id = arch_recs(i).id
and key = arch_recs(i).key;
end;
/
Upvotes: 1
Reputation: 6278
You should be able to do something like:
create table temp_keys as (select pk from yourtable where condition);
select pk from yourtable where pk in (select pk from temp_keys) for update;
insert into archivetable (columnlist)
select columnlist from yourtable
where pk in (select pk from temp_keys);
commit;
drop table temp_keys;
You could also use a temporary table for temp_keys, then you don't have to drop it every time.
EDIT: With the new information you added you can skip the select for update. Just keep track of the ids you have copied for the following delete. Create a (possibly temporary) table with ids, do the insert, do the delete and you are done.
Upvotes: 1
Reputation: 31676
In Oracle 12c and above, you could achieve this using In-Database Archiving
.
Enabling in-database archiving on the table results in the addition of a system generated hidden column called ORA_ARCHIVE_STATE
It uses a concept of "mark for delete", so the data remains present in the table, but is not visible to the application.
Firstly, enable in-database archiving for your table.
ALTER TABLE yourtable ROW ARCHIVAL;
So, a hidden column called ORA_ARCHIVE_STATE
is created for the table which you could check using user_tab_cols
Now, make your rows invisible to other applications/sessions,
UPDATE yourtable
SET ORA_ARCHIVE_STATE = '1'
WHERE id BETWEEN 1 AND 10000;
COMMIT;
Now, You may delete these rows whenever you like using the condition.
Refer In-Database Archiving in Oracle Database 12c Release 1 (12.1)
Upvotes: 1