序文
このアプリケーションは、DELETE
クエリを並列に実行するいくつかのスレッドを実行します。クエリは分離されたデータに影響します。つまり、別々のスレッドの同じ行でDELETE
が同時に発生する可能性はありません。ただし、ドキュメントごとに、MySQLはDELETE
ステートメントにいわゆる「次のキー」ロックを使用します。これにより、一致するキーと一部のギャップの両方がロックされます。これによりデッドロックが発生し、READ COMMITTED
分離レベルを使用するのが唯一の解決策です。
問題
巨大なテーブルのDELETE
sで複雑なJOIN
ステートメントを実行すると、問題が発生します。特定のケースでは、警告が2行しかないテーブルがありますが、クエリは、2つの個別のINNER JOIN
edテーブルから特定のエンティティに属するすべての警告を削除する必要があります。クエリは次のとおりです。
DELETE pw
FROM proc_warnings pw
INNER JOIN day_position dp
ON dp.transaction_id = pw.transaction_id
INNER JOIN ivehicle_days vd
ON vd.id = dp.ivehicle_day_id
WHERE vd.ivehicle_id=? AND dp.dirty_data=1
Day_positionテーブルが十分に大きい場合(私のテストケースでは1448行あります)、トランザクションがREAD COMMITTED
分離モードブロックでもentireproc_warnings
テーブル。
問題は常にこのサンプルデータで再現されます- http://yadi.sk/d/QDuwBtpW1BxB9 MySQL 5.1(5.1.59でチェック)とMySQL 5.5(MySQL 5.5.24でチェック)の両方で。
編集:リンクされたサンプルデータには、クエリテーブルのスキーマとインデックスも含まれています。
CREATE TABLE `proc_warnings` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`transaction_id` int(10) unsigned NOT NULL,
`warning` varchar(2048) NOT NULL,
PRIMARY KEY (`id`),
KEY `proc_warnings__transaction` (`transaction_id`)
);
CREATE TABLE `day_position` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`transaction_id` int(10) unsigned DEFAULT NULL,
`sort_index` int(11) DEFAULT NULL,
`ivehicle_day_id` int(10) unsigned DEFAULT NULL,
`dirty_data` tinyint(4) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `day_position__trans` (`transaction_id`),
KEY `day_position__is` (`ivehicle_day_id`,`sort_index`),
KEY `day_position__id` (`ivehicle_day_id`,`dirty_data`)
) ;
CREATE TABLE `ivehicle_days` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`d` date DEFAULT NULL,
`sort_index` int(11) DEFAULT NULL,
`ivehicle_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `ivehicle_days__is` (`ivehicle_id`,`sort_index`),
KEY `ivehicle_days__d` (`d`)
);
トランザクションごとのクエリは次のとおりです。
トランザクション1
set transaction isolation level read committed;
set autocommit=0;
begin;
DELETE pw
FROM proc_warnings pw
INNER JOIN day_position dp
ON dp.transaction_id = pw.transaction_id
INNER JOIN ivehicle_days vd
ON vd.id = dp.ivehicle_day_id
WHERE vd.ivehicle_id=2 AND dp.dirty_data=1;
トランザクション2
set transaction isolation level read committed;
set autocommit=0;
begin;
DELETE pw
FROM proc_warnings pw
INNER JOIN day_position dp
ON dp.transaction_id = pw.transaction_id
INNER JOIN ivehicle_days vd
ON vd.id = dp.ivehicle_day_id
WHERE vd.ivehicle_id=13 AND dp.dirty_data=1;
それらの1つは常に「ロック待機タイムアウトを超えました...」エラーで失敗します。 information_schema.innodb_trx
には次の行が含まれます:
| trx_id | trx_state | trx_started | trx_requested_lock_id | trx_wait_started | trx_wait | trx_mysql_thread_id | trx_query |
| '1A2973A4' | 'LOCK WAIT' | '2012-12-12 20:03:25' | '1A2973A4:0:3172298:2' | '2012-12-12 20:03:25' | '2' | '3089' | 'DELETE pw FROM proc_warnings pw INNER JOIN day_position dp ON dp.transaction_id = pw.transaction_id INNER JOIN ivehicle_days vd ON vd.id = dp.ivehicle_day_id WHERE vd.ivehicle_id=13 AND dp.dirty_data=1' |
| '1A296F67' | 'RUNNING' | '2012-12-12 19:58:02' | NULL | NULL | '7' | '3087' | NULL |
information_schema.innodb_locks
| lock_id | lock_trx_id | lock_mode | lock_type | lock_table | lock_index | lock_space | lock_page | lock_rec | lock_data |
| '1A2973A4:0:3172298:2' | '1A2973A4' | 'X' | 'RECORD' | '`deadlock_test`.`proc_warnings`' | '`PRIMARY`' | '0' | '3172298' | '2' | '53' |
| '1A296F67:0:3172298:2' | '1A296F67' | 'X' | 'RECORD' | '`deadlock_test`.`proc_warnings`' | '`PRIMARY`' | '0' | '3172298' | '2' | '53' |
両方のクエリで、主キー= 53の行に排他的なX
ロックが必要なことがわかります。ただし、どちらもproc_warnings
テーブルから行を削除する必要はありません。インデックスがロックされている理由がわかりません。さらに、proc_warnings
テーブルが空である場合、またはday_position
テーブルに含まれる行数が少ない場合(つまり、100行)でも、インデックスはロックされません。
さらなる調査は、同様のEXPLAIN
クエリに対してSELECT
を実行することでした。これは、クエリオプティマイザーがproc_warnings
テーブルのクエリにインデックスを使用しないことを示しています。これが、主キーインデックス全体をブロックする理由を私が想像できる唯一の理由です。
簡略化されたケース
また、レコードが2つあるテーブルが2つしかなく、子テーブルの親テーブルのref列にインデックスがない場合も、問題はより簡単なケースで再現できます。
parent
テーブルを作成する
CREATE TABLE `parent` (
`id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
child
テーブルを作成する
CREATE TABLE `child` (
`id` int(10) unsigned NOT NULL,
`parent_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
テーブルを埋める
INSERT INTO `parent` (id) VALUES (1), (2);
INSERT INTO `child` (id, parent_id) VALUES (1, NULL), (2, NULL);
2つの並列トランザクションでテストします。
トランザクション1
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET AUTOCOMMIT=0;
BEGIN;
DELETE c FROM child c
INNER JOIN parent p ON p.id = c.parent_id
WHERE p.id = 1;
トランザクション2
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET AUTOCOMMIT=0;
BEGIN;
DELETE c FROM child c
INNER JOIN parent p ON p.id = c.parent_id
WHERE p.id = 2;
どちらの場合にも共通するのは、MySQLがインデックスを使用しないことです。それがテーブル全体をロックする理由だと思います。
私たちのソリューション
現時点で確認できる唯一の解決策は、デフォルトのロック待機タイムアウトを50秒から500秒に増やして、スレッドのクリーンアップを完了することです。次に、指を交差させます。
助けてくれてありがとう。
新しい回答(MySQLスタイルの動的SQL):わかりました。これは、他の投稿者が説明した方法で問題に取り組みます-相互に互換性のない排他ロックが取得される順序を逆にすることで、発生するロックの数に関係なく、発生するのはトランザクション実行の最後の最短時間。
これは、ステートメントの読み取り部分をそれ自体の選択ステートメントに分離し、ステートメントの出現順序が原因で最後に実行するよう強制され、proc_warningsテーブルにのみ影響する削除ステートメントを動的に生成することによって実現されます。
デモはsql fiddleで入手できます。
この link は、サンプルデータを含むスキーマと、ivehicle_id=2
に一致する行の簡単なクエリを示しています。削除されていないため、2行になります。
この link は同じスキーマ、サンプルデータを示していますが、値2をDeleteEntriesストアドプログラムに渡し、SPにproc_warnings
のivehicle_id=2
エントリを削除するように指示しています。単純なクエリすべての行が正常に削除されたため、行は結果を返しません。デモリンクは、コードが意図したとおりに機能することを示すだけです。適切なテスト環境のユーザーは、ブロックされたスレッドの問題が解決されるかどうかについてコメントできます。
便宜上、ここにもコードがあります。
CREATE PROCEDURE DeleteEntries (input_vid INT)
BEGIN
SELECT @idstring:= '';
SELECT @idnum:= 0;
SELECT @del_stmt:= '';
SELECT @idnum:= @idnum+1 idnum_col, @idstring:= CONCAT(@idstring, CASE WHEN CHARACTER_LENGTH(@idstring) > 0 THEN ',' ELSE '' END, CAST(id AS CHAR(10))) idstring_col
FROM proc_warnings
WHERE EXISTS (
SELECT 0
FROM day_position
WHERE day_position.transaction_id = proc_warnings.transaction_id
AND day_position.dirty_data = 1
AND EXISTS (
SELECT 0
FROM ivehicle_days
WHERE ivehicle_days.id = day_position.ivehicle_day_id
AND ivehicle_days.ivehicle_id = input_vid
)
)
ORDER BY idnum_col DESC
LIMIT 1;
IF (@idnum > 0) THEN
SELECT @del_stmt:= CONCAT('DELETE FROM proc_warnings WHERE id IN (', @idstring, ');');
PREPARE del_stmt_hndl FROM @del_stmt;
EXECUTE del_stmt_hndl;
DEALLOCATE PREPARE del_stmt_hndl;
END IF;
END;
これは、トランザクション内からプログラムを呼び出す構文です。
CALL DeleteEntries(2);
元の回答(まだ粗末ではないと思います)2つの問題のように見えます:1)クエリが遅い2)予期しないロック動作
問題#1に関しては、遅いクエリは、タンデムクエリステートメントの簡素化とインデックスへの便利な追加または変更における同じ2つの手法によって解決されることがよくあります。あなた自身はすでにインデックスへの接続を作成しています-それらがないと、オプティマイザは処理する行の限定されたセットを検索できず、追加の行ごとに乗算する各テーブルの各行は、実行する必要がある追加の作業量をスキャンしました。
参照後に改訂POST:ただし、適切なインデックス構成があることを確認することで、クエリのパフォーマンスが最大になると思います。そうするために、削除のパフォーマンスが向上し、さらに大きなインデックスのトレードオフと、おそらく追加のインデックス構造が追加された同じテーブルでの挿入のパフォーマンスが著しく遅くなり、削除パフォーマンスが向上する可能性があります。
より良いもの:
CREATE TABLE `day_position` (
...,
KEY `day_position__id_rvrsd` (`dirty_data`, `ivehicle_day_id`)
) ;
CREATE TABLE `ivehicle_days` (
...,
KEY `ivehicle_days__vid_no_sort_index` (`ivehicle_id`)
);
ここでも修正:実行に時間がかかるため、dirty_dataをインデックスに残しておきます。インデックスの順序でivehicle_day_idの後に配置した場合も、間違いです。
しかし、私がそれを手にした場合、現時点では、それを長くするのに十分な量のデータが必要になるため、すべてのカバーするインデックスを使用して、最高のインデックスを取得していることを確認します。問題のその部分を除外する他に何もない場合、私のトラブルシューティングの時間は買うことができます。
ベスト/カバーインデックス:
CREATE TABLE `day_position` (
...,
KEY `day_position__id_rvrsd_trnsid_cvrng` (`dirty_data`, `ivehicle_day_id`, `transaction_id`)
) ;
CREATE TABLE `ivehicle_days` (
...,
UNIQUE KEY `ivehicle_days__vid_id_cvrng` (ivehicle_id, id)
);
CREATE TABLE `proc_warnings` (
.., /*rename primary key*/
CONSTRAINT pk_proc_warnings PRIMARY KEY (id),
UNIQUE KEY `proc_warnings__transaction_id_id_cvrng` (`transaction_id`, `id`)
);
最後の2つの変更提案によって求められるパフォーマンス最適化の目標は2つあります。
1)連続してアクセスされるテーブルの検索キーが、現在アクセスされているテーブルに対して返されるクラスター化されたキーの結果と同じでない場合は、index-seek-withの2番目のセットを作成する必要があったものを排除しますクラスター化インデックスのスキャン操作
2)後者が当てはまらない場合でも、インデックスは必要な結合キーをソート順に保持するため、少なくともオプティマイザがより効率的な結合アルゴリズムを選択できる可能性があります。
クエリはできる限り簡略化されているようです(後で編集する場合に備えて、ここにコピーします)。
DELETE pw
FROM proc_warnings pw
INNER JOIN day_position dp
ON dp.transaction_id = pw.transaction_id
INNER JOIN ivehicle_days vd
ON vd.id = dp.ivehicle_day_id
WHERE vd.ivehicle_id=2 AND dp.dirty_data=1;
もちろん、クエリオプティマイザーの進行方法に影響を与える結合順序に関する記述がない場合は、他の人が提供した書き換え提案のいくつかを試すことができます。
DELETE FROM proc_warnings
FORCE INDEX (`proc_warnings__transaction_id_id_cvrng`, `pk_proc_warnings`)
WHERE EXISTS (
SELECT 0
FROM day_position
FORCE INDEX (`day_position__id_rvrsd_trnsid_cvrng`)
WHERE day_position.transaction_id = proc_warnings.transaction_id
AND day_position.dirty_data = 1
AND EXISTS (
SELECT 0
FROM ivehicle_days
FORCE INDEX (`ivehicle_days__vid_id_cvrng`)
WHERE ivehicle_days.id = day_position.ivehicle_day_id
AND ivehicle_days.ivehicle_id = ?
)
);
#2に関しては、予期しないロック動作。
両方のクエリで、主キー= 53の行に排他的Xロックが必要なことがわかります。ただし、どちらもproc_warningsテーブルから行を削除する必要はありません。インデックスがロックされている理由がわかりません。
ロックされるデータの行はクラスター化インデックス内にあるため、つまり、データの単一行自体がインデックスに存在するため、ロックされるのはインデックスだと思います。
次の理由でロックされます。
1) http://dev.mysql.com/doc/refman/5.1/en/innodb-locks-set.html によると==
... DELETEは通常、SQLステートメントの処理でスキャンされるすべてのインデックスレコードにレコードロックを設定します。行を除外するステートメントにWHERE条件があるかどうかは関係ありません。 InnoDBは正確なWHERE条件を記憶していませんが、スキャンされたインデックス範囲のみを認識しています。
また、上記のとおりです。
...私の場合、READ COMMITTEDの主な機能は、ロックの処理方法です。一致しない行のインデックスロックを解放する必要がありますが、解放しません。
そのための次の参照を提供しました:
http://dev.mysql.com/doc/refman/5.1/en/set-transaction.html#isolevel_read-committed
これはあなたと同じですが、同じ参照に従って、ロックが解放される条件があることを除いて、
また、MySQLがWHERE条件を評価した後に、一致しない行のレコードロックが解放されます。
これはこのマニュアルページでも繰り返されます http://dev.mysql.com/doc/refman/5.1/en/innodb-record-level-locks.html
READ COMMITTED分離レベルを使用したり、innodb_locks_unsafe_for_binlogを有効にしたりすると、他にも影響があります。MySQLがWHERE条件を評価した後、一致しない行のレコードロックが解放されます。
したがって、ロックを解除する前にWHERE条件を評価する必要があると言われています。残念ながら、WHERE条件がいつ評価されるかはわかりません。おそらく、オプティマイザによって作成されたプランから別のプランに変更される可能性があります。しかし、それはロックの解放がクエリ実行のパフォーマンスに何らかの形で依存していることを教えてくれます。前述のように、その最適化はステートメントの注意深い記述とインデックスの賢明な使用に依存しています。テーブル設計を改善することで改善することもできますが、それはおそらく別の質問に任せるのが最善です。
さらに、proc_warningsテーブルが空の場合もインデックスはロックされません。
インデックス内のレコードがない場合、データベースはレコードをロックできません。
さらに、... day_positionテーブルに含まれる行数が少ない場合(つまり、100行)、インデックスはロックされません。
これは、統計情報の変更による異なる実行プラン、非常に小さいデータセットによる非常に高速な実行による速すぎる観察ロックなど、多くのことを意味しますが、これらに限定されません。結合操作。
READ_COMMITTEDがこの状況をどのように引き起こしているのかがわかります。
READ_COMMITTED は3つのことを可能にします:
トランザクションは以下との接触を維持する必要があるため、トランザクション自体に内部パラダイムが作成されます。
2つの異なるREAD_COMMITTEDトランザクションが同じ方法で更新されている同じテーブル/行にアクセスしている場合は、テーブルロックではなく gen_clust_index(akaクラスタ化インデックス) 。あなたの単純化されたケースからのクエリを考えると:
トランザクション1
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET AUTOCOMMIT=0;
BEGIN;
DELETE c FROM child c
INNER JOIN parent p ON p.id = c.parent_id
WHERE p.id = 1;
トランザクション2
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET AUTOCOMMIT=0;
BEGIN;
DELETE c FROM child c
INNER JOIN parent p ON p.id = c.parent_id
WHERE p.id = 2;
Gen_clust_indexの同じ場所をロックしています。 「しかし、各トランザクションは異なる主キーを持っている」と言うかもしれません。残念ながら、これはInnoDBの目には当てはまりません。 id 1とid 2が同じページにあるのは、たまたまです。
質問で入力したinformation_schema.innodb_locks
を振り返ってください
| lock_id | lock_trx_id | lock_mode | lock_type | lock_table | lock_index | lock_space | lock_page | lock_rec | lock_data |
| '1A2973A4:0:3172298:2' | '1A2973A4' | 'X' | 'RECORD' | '`deadlock_test`.`proc_warnings`' | '`PRIMARY`' | '0' | '3172298' | '2' | '53' |
| '1A296F67:0:3172298:2' | '1A296F67' | 'X' | 'RECORD' | '`deadlock_test`.`proc_warnings`' | '`PRIMARY`' | '0' | '3172298' | '2' | '53' |
lock_id
、lock_trx_id
を除いて、残りのロックの説明は同じです。トランザクションは同じレベルの競争条件にあるため(同じトランザクション分離)、これは実際に発生するはずです。
私を信じて、私は以前にこのような状況に対処したことがあります。これに関する私の過去の投稿は次のとおりです。
Nov 05, 2012
: 挿入クエリのデッドロックのinnodbステータスを分析する方法Aug 08, 2011
: InnoDBデッドロックはINSERT/UPDATE/DELETE専用ですか?Jun 14, 2011
: 時々遅いクエリの理由?Jun 08, 2011
: これら2つのクエリを順番に実行するとデッドロックが発生しますか?Jun 06, 2011
: innodbステータスログのデッドロックを解読する際の問題クエリと説明を確認しました。よくわかりませんが、問題は次のとおりだと直感しています。クエリを見てみましょう:
DELETE pw
FROM proc_warnings pw
INNER JOIN day_position dp
ON dp.transaction_id = pw.transaction_id
INNER JOIN ivehicle_days vd
ON vd.id = dp.ivehicle_day_id
WHERE vd.ivehicle_id=? AND dp.dirty_data=1;
同等のSELECTは次のとおりです。
SELECT pw.id
FROM proc_warnings pw
INNER JOIN day_position dp
ON dp.transaction_id = pw.transaction_id
INNER JOIN ivehicle_days vd
ON vd.id = dp.ivehicle_day_id
WHERE vd.ivehicle_id=16 AND dp.dirty_data=1;
その説明を見ると、実行プランがproc_warnings
テーブルで始まっていることがわかります。つまり、MySQLはテーブルの主キーをスキャンし、各行について条件がtrueであるかどうかを確認します。trueの場合は、行が削除されます。つまり、MySQLは主キー全体をロックする必要があります。
必要なのは、JOIN順序を逆にすることです。つまり、vd.ivehicle_id=16 AND dp.dirty_data=1
ですべてのトランザクションIDを見つけ、proc_warnings
テーブルで結合します。
つまり、インデックスの1つにパッチを適用する必要があります。
ALTER TABLE `day_position`
DROP INDEX `day_position__id`,
ADD INDEX `day_position__id`
USING BTREE (`ivehicle_day_id`, `dirty_data`, `transaction_id`);
削除クエリを書き換えます。
DELETE pw
FROM (
SELECT DISTINCT dp.transaction_id
FROM ivehicle_days vd
JOIN day_position dp ON dp.ivehicle_day_id = vd.id
WHERE vd.ivehicle_id=? AND dp.dirty_data=1
) as tr_id
JOIN proc_warnings pw ON pw.transaction_id = tr_id.transaction_id;
方法を指定せずにトランザクションレベルを設定すると、次のトランザクションのみにコミット済み読み取りが適用されるため、(自動コミットを設定する)。これは、autocommit = 0の後、Read Committedにはいなくなったことを意味します。私はそれをこのように書きます:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
DELETE c FROM child c
INNER JOIN parent p ON
p.id = c.parent_id
WHERE p.id = 1;
クエリを実行することで、現在の分離レベルを確認できます
SELECT @@tx_isolation;