A question came up on Oracle-L recently about possible locking anomalies with deferrable referential integrity constraints.
An update by primary key is taking a long time; the update sets several columns, one of which is the child end of a referential integrity constraint. A check on v$active_session_history shows lots of waits for “enq: TX – row lock contention” in mode 4 (share), and many of these waits also identify the current object as the index that has been created to avoid the “foreign key locking” problem on this constraint (though many of the waits show the current_obj# as -1). A possible key feature of the issue is that foreign key constraint is defined as “deferrable initially deferred”. The question is, could such a constraint result in TX/4 waits.
My initial thought was that if the constraint was deferrable it was unlikely, there would have to be other features coming into play.
Of course, when the foreign key is NOT deferrable it’s easy to set up cases where a TX/4 appears: for example you insert a new parent value without issuing a commit then I insert a new matching child, at that point my session will wait for your session to commit or rollback. If you commit my insert succeeds if you rollback my session raises an error (ORA-02291: integrity constraint (schema_name.constraint_name) violated – parent key not found). But if the foreign key is deferred the non-existence (or potential existence, or not) of the parent should matter. If the constraint is deferrable, though, the first guess would be that you could get away with things like this so long as you fixed up the data in time for the commit.
I was wrong. Here’s a little example:
create table parent ( id number(4), name varchar2(10), constraint par_pk primary key (id) ) ; create table child( id_p number(4) constraint chi_fk_par references parent deferrable initially deferred, id number(4), name varchar2(10), constraint chi_pk primary key (id_p, id) ) ; insert into parent values (1,'Smith'); insert into parent values (2,'Jones'); insert into child values(1,1,'Simon'); insert into child values(1,2,'Sally'); insert into child values(2,1,'Jack'); insert into child values(2,2,'Jill'); commit; begin dbms_stats.gather_table_stats(user,'parent'); dbms_stats.gather_table_stats(user,'child'); end; / pause Press return update child set id_p = 3 where id_p = 2 and id = 2;
If you don’t do anything after the pause and before the insert then the update will succeed – but fail on a subsequent commit unless you insert parent 3 before committing. But if you take advantage of the pause to use another session to insert parent 3 first, the update will then hang waiting for the parent insert to commit or rollback – and what happens next may surprise you. Basically the deferrability doesn’t protect you from the side effects of conflicting transactions.
The variations on what can happen next (insert the parent elsewhere, commit or rollback) are interesting and left as an exercise.
I was slightly surprised to find that I had had a conversation about this sort of thing some time ago, triggered by a comment to an earlier post. If you want to read a more thorough investigation of the things that can happen and how deferrable RI works then there’s a good article at this URL.