ablog

不器用で落着きのない技術者のメモ

It's possible for FKs to cause TX row lock contention

RE: Tx - row lock contention after implementing transaction management in application server - oracle-l - FreeLists
に面白い投稿があったので、自分でも試してみた。
外部キーがトランザクションエンキュー(行ロック)競合の原因になるケースがあるらしい。
以下のようなケースで発生する。

  • セッション1で親テーブルにレコードAをinsertして、commit/rollbackしていない状態のままにする。
  • セッション2で子テーブルにレコードAを外部参照するレコードをinsertしようとする。
  • セッション2は「TX - row lock contention」で待機する。

日本語が正確じゃない気がする。。。
実際に試してみる。

  • テーブルを作成する。
$ sqlplus scott/tiger
SQL> create table parent(p_id number primary key);
SQL> create table child(c_id number primary key, p_id number references parent(p_id));
  • あるセッションで親テーブルにinsertする。
$ sqlplus scott/tiger
SQL> insert into parent values(1);

1 row created.
  • 別セッションで子テーブルにinsertする。
$ sqlplus scott/tiger
SQL> insert into child values(1,1);
  • ロックの競合状況を見てみる。
$ sqlplus scott/tiger
SQL> set feedback off
set pagesize 9999
set linesize 2000
set trimspool on
set trimout on

col block for 9999
col sid for 9999
col status for a8
col machine for a9
col username for a9
col type for a2
col lmode for 99
col request for 99
col program for a10
col serialno for 999999
col lock_time for a10
col sql for a20

select a.blocking_session block,
        a.sid sid,
        a.status status,
        substr(a.machine, 1,9) machine,
        a.username username,
        b.type type,
        b.lmode lmode,
        b.request request,
        a.program program,
        a.serial# serialno,
        to_char(b.ctime/60, '9990.9') lock_time,
        c.sql_text sql
from v$session a,
        v$lock b,
        v$sqlarea c
where a.sid=b.sid
        and a.sql_address = c.address;

BLOCK   SID STATUS   MACHINE   USERNAME  TY LMODE REQUEST PROGRAM    SERIALNO LOCK_TIME  SQL
----- ----- -------- --------- --------- -- ----- ------- ---------- -------- ---------- --------------------
  193   202 ACTIVE   solaris10 SCOTT     TX     0       4 sqlplus@so      225     0.1    insert into child va
                                                          laris101 (                     lues(1,1)
                                                          TNS V1-V3)

  193   202 ACTIVE   solaris10 SCOTT     TM     2       0 sqlplus@so      225     0.1    insert into child va
                                                          laris101 (                     lues(1,1)
                                                          TNS V1-V3)

  193   202 ACTIVE   solaris10 SCOTT     TM     3       0 sqlplus@so      225     0.1    insert into child va
                                                          laris101 (                     lues(1,1)
                                                          TNS V1-V3)

  193   202 ACTIVE   solaris10 SCOTT     TX     6       0 sqlplus@so      225     0.1    insert into child va
                                                          laris101 (                     lues(1,1)
                                                          TNS V1-V3)
  • 親テーブルへのinsertをcommitする。
SQL> commit;
  • 子テーブルへのinsertが実行される。
1 row created.
  • ロック待ちが解消されている。
SQL> r
  1  select a.blocking_session block,
  2          a.sid sid,
  3          a.status status,
  4          substr(a.machine, 1,9) machine,
  5          a.username username,
  6          b.type type,
  7          b.lmode lmode,
  8          b.request request,
  9          a.program program,
 10          a.serial# serialno,
 11          to_char(b.ctime/60, '9990.9') lock_time,
 12          c.sql_text sql
 13  from v$session a,
 14          v$lock b,
 15          v$sqlarea c
 16  where a.sid=b.sid
 17*         and a.sql_address = c.address

no rows selected


ちなみに親テーブルへのinsertをrollbackすると、子テーブルへinsertしようとしているセッションでは ORA-02291 が発生する。

insert into child values(1,1)
*
ERROR at line 1:
ORA-02291: integrity constraint (SCOTT.SYS_C004448) violated - parent key not
found

普通に考えると当たり前の話だな。というかこうなってもらわないと困る。
子テーブルの外部キーにセットできる値は「親テーブルに存在する値」か「NULL」。上記の例では、親テーブルへのinsertがcommitされるかrollbackされるかで、「親テーブルに存在するか否か」が変わるからcommit/rollbackされるまで待たないといけない。