INSERT 1文だけでもデッドロックするという話

下記の記事のように複数行に対する UPDATE は BEGIN の無い 1 文でもデッドロックすることがあります。

これを見て思ったのですが INSERT であればバルクではない 1 行だけでもデッドロックすることがあります。

次のようなテーブルを用意します。

create table s (
    sid int not null primary key
);

create table t (
    id int not null primary key,
    sid int not null,
    foreign key (sid) references s (sid)
);

次のクエリでデッドロックします。

insert into t values (1,1);

実際に試してみます。次のように INSERT を並列にたくさん実行します。

seq 1 100 | xargs -P0 -i mysql test -u root -e 'insert into t values (1,1)'

外部キーの参照先が存在しないので外部キー制約のエラーがたくさん発生しますが、それに混じってデッドロックも発生します。

ERROR 1452 (23000) at line 1: Cannot add or update a child row: a foreign key constraint fails (`test`.`t`, CONSTRAINT `t_ibfk_1` FOREIGN KEY (`sid`) REFERENCES `s` (`sid`))
ERROR 1452 (23000) at line 1: Cannot add or update a child row: a foreign key constraint fails (`test`.`t`, CONSTRAINT `t_ibfk_1` FOREIGN KEY (`sid`) REFERENCES `s` (`sid`))
ERROR 1213 (40001) at line 1: Deadlock found when trying to get lock; try restarting transaction
ERROR 1452 (23000) at line 1: Cannot add or update a child row: a foreign key constraint fails (`test`.`t`, CONSTRAINT `t_ibfk_1` FOREIGN KEY (`sid`) REFERENCES `s` (`sid`))
ERROR 1452 (23000) at line 1: Cannot add or update a child row: a foreign key constraint fails (`test`.`t`, CONSTRAINT `t_ibfk_1` FOREIGN KEY (`sid`) REFERENCES `s` (`sid`))

原因

実は外部キー制約は関係なく、INSERT が ROLLBACK されることに意味があります。次のように手操作でも簡単に発生させられます。

↑の例から外部キー制約も外した次のようなテーブルを用意します。

create table t (
  id int not null primary key
);

最初にトランザクションを開始して行を挿入します。

/* Tx.1 */
begin;
insert into t values (1);

次に、2つのトランザクションで同じ行を挿入します。これは↑のトランザクションと競合するのでロック待ちになります。

/* Tx.2 */
insert into t values (1);

/* Tx.3 */
insert into t values (1);

そして最初のトランザクションをロールバックします。

/* Tx.1 */
rollback;

すると、ロック待ちだった2番目と3番目のどちらかがデッドロックによりエラーになります。

/* Tx.3 */
insert into t values (1);
/* ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction */

なぜこれがデッドロックするかは次のページに解説があるので割愛します(Tx.2 Tx.3 が共有ロックでロック待ちになったあと、Tx.1 がロールバックされたことで排他ロックを取ろうとしてデッドロック)。

最初に示した例は外部キー制約でエラーにすることで、明示的にトランザクションを開始させることなくロールバックさせている、ということになります。