web-dev-qa-db-ja.com

SELECT ... ORDER BY xxx LIMIT 1 FOR UPDATEによってロックされる行数はいくつですか?

次の構造のクエリがあります。

SELECT ..... WHERE status = 'QUEUED' ORDER BY position ASC LIMIT 1 FOR UPDATE;

これは、InnoDBテーブルに対する単一テーブルのSELECTステートメントです。フィールドposition(INT NOT NULL)にはインデックスがあります。ステータスはENUMであり、インデックスも付けられます。

SELECT ... FOR UPDATEマニュアルページには、読み取ったすべての行がロックされると書かれています。この場合、1つの行だけがロックされることを正しく理解していますか?それとも、テーブル全体をロックしますか?

EXPLAINクエリでロックされる行を決定することは可能ですか?はいの場合-どのように?空のテーブルに対するクエリの説明は、次のことを示しています。

1;'SIMPLE';'job';'index';<null>;'index_position';[34,...];<null>;1;'Using where'
33

これは素晴らしい質問です。 InnoDBは行レベルのロックエンジンですが、バイナリログ(レプリケーションに使用されます。ポイントインタイムリカバリ)で安全性を確保するために、追加のロックを設定する必要があります。説明を始めるには、次の(単純な)例を検討してください。

session1> START TRANSACTION;
session1> DELETE FROM users WHERE is_deleted = 1; # 1 row matches (user_id 10), deleted.
session2> START TRANSACTION;
session2> UPDATE users SET is_deleted = 1 WHERE user_id = 5; # 1 row matches.
session2> COMMIT;
session1> COMMIT;

ステートメントはコミットされるとバイナリログにのみ書き込まれるため、スレーブセッション#2が最初に適用され、異なる結果が生成されデータが破損します

つまり、InnoDBが行うことは、追加のロックを設定することです。 is_deletedにインデックスが付けられている場合、session1がコミットする前に、他の誰もis_deleted=1のレコードのまたは範囲に挿入を変更できません。 is_deletedにインデックスがない場合、InnoDBはテーブル全体のすべての行をロックして、再生が同じ順序になるようにする必要があります。これは、ギャップのロックと考えることができます。これは、行レベルのロックから直接把握するための異なる概念です

そのORDER BY position ASCの場合、InnoDBは、最小のキー値と「特別な」最小の値の間で新しい行が変更されないようにする必要があります。 ORDER BY position DESC ..のようなことをうまくやった場合、誰もこの範囲に挿入できません。

それで解決策が来ます:

  • ステートメントベースのバイナリロギングは最悪です。私たち全員が 行ベースのバイナリロギング (MySQL 5.1から利用可能ですが、デフォルトではオンになっていません)に切り替える未来を本当に楽しみにしています。

  • 行ベースのレプリケーションでは、分離レベルを読み取りコミットに変更すると、一致する1つの行のみをロックする必要があります。

  • マゾヒストになりたい場合は、ステートメントベースのレプリケーションで innodb_locks_unsafe_for_binlog をオンにすることもできます。


4月22日更新:テストケースの改善されたバージョンをコピーして貼り付ける(「ギャップ内で」検索していませんでした):

session1> CREATE TABLE test (id int not null primary key auto_increment, data1 int, data2 int, INDEX(data1)) engine=innodb;
Query OK, 0 rows affected (0.00 sec)

session1> INSERT INTO test VALUES (NULL, 1, 2), (NULL, 2, 1), (5, 2, 2), (6, 3, 3), (3, 3, 4), (4, 4, 3);
Query OK, 6 rows affected (0.00 sec)
Records: 6  Duplicates: 0  Warnings: 0

session1> start transaction;
Query OK, 0 rows affected (0.00 sec)

session1> SELECT id FROM test ORDER BY data1 LIMIT 1 FOR UPDATE;
+----+
| id |
+----+
|  1 |
+----+
1 row in set (0.00 sec)

session2> INSERT INTO test values (NULL, 0, 99); # blocks - 0 is in the gap between the lowest value found (1) and the "special" lowest value.

# At the same time, from information_schema:

localhost information_schema> select * from innodb_locks\G
*************************** 1. row ***************************
    lock_id: 151A1C:1735:4:2
lock_trx_id: 151A1C
  lock_mode: X,GAP
  lock_type: RECORD
 lock_table: `so5694658`.`test`
 lock_index: `data1`
 lock_space: 1735
  lock_page: 4
   lock_rec: 2
  lock_data: 1, 1
*************************** 2. row ***************************
    lock_id: 151A1A:1735:4:2
lock_trx_id: 151A1A
  lock_mode: X
  lock_type: RECORD
 lock_table: `so5694658`.`test`
 lock_index: `data1`
 lock_space: 1735
  lock_page: 4
   lock_rec: 2
  lock_data: 1, 1
2 rows in set (0.00 sec)

# Another example:
select * from test where id < 1 for update; # blocks
26
Morgan Tocker

私はテストをしました。次のテーブルを作成しました。

id  data1   data2
1   1   2
2   2   1
5   2   2
6   3   3
3   3   4
4   4   3

次に、トランザクションとの最初の接続を作成しました。

SELECT id FROM test ORDER BY data1 LIMIT 1 FOR UPDATE;

結果はid = 1の行でした。

次に、最初にコミットせずに、別の接続から2番目のトランザクションを作成しました。

SELECT id FROM test WHERE data1=2 FOR UPDATE;

それはブロックしませんでした。そして、最初のトランザクションで選択された行そのものを選択しようとしたときにのみブロックされました。 ORDERBYをDESCに変更して以下を試しましたが動作します。

結論:MySQLは、ORDER BY句とLIMIT句を使用するときに実際に選択した行のみをブロックします。ギャップロックの説明については、@ Morganの回答を参照してください。

私のMySQLバージョンは5.0.45です

2

MySQLの一部のバージョンにはバグがあります: #67745 UPDATE、LIMIT、およびORDER BYにSELECTを使用すると行ロックが多すぎます

バージョン:5.5.28、5.5.30、5.7.1

私のローカルmysql5.5.25win64の同じバグ。

2
dtmax1

他のデータベースとは異なり、MySQLではクエリがインデックス位置をロックします。これは事実上、現在status'QUEUED'に等しいか、別のトランザクションから'QUEUED'に変更したいすべての行がロックされることを意味します。これに対して私が見つけた唯一の解決策は、FOR UPDATEのない行を選択し、IDベースのフィルターを使用してそれらを選択し、ロックされたら条件を再確認することです。ニースではありませんが、それは仕事をします。

1