web-dev-qa-db-ja.com

MySQL InnoDBは、READ COMMITTEDであっても削除時に主キーをロックします

序文

このアプリケーションは、DELETEクエリを並列に実行するいくつかのスレッドを実行します。クエリは分離されたデータに影響します。つまり、別々のスレッドの同じ行でDELETEが同時に発生する可能性はありません。ただし、ドキュメントごとに、MySQLはDELETEステートメントにいわゆる「次のキー」ロックを使用します。これにより、一致するキーと一部のギャップの両方がロックされます。これによりデッドロックが発生し、READ COMMITTED分離レベルを使用するのが唯一の解決策です。

問題

巨大なテーブルのDELETEsで複雑なJOINステートメントを実行すると、問題が発生します。特定のケースでは、警告が2行しかないテーブルがありますが、クエリは、2つの個別のINNER JOINedテーブルから特定のエンティティに属するすべての警告を削除する必要があります。クエリは次のとおりです。

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秒に増やして、スレッドのクリーンアップを完了することです。次に、指を交差させます。

助けてくれてありがとう。

11
vitalidze

新しい回答(MySQLスタイルの動的SQL):わかりました。これは、他の投稿者が説明した方法で問題に取り組みます-相互に互換性のない排他ロックが取得される順序を逆にすることで、発生するロックの数に関係なく、発生するのはトランザクション実行の最後の最短時間。

これは、ステートメントの読み取り部分をそれ自体の選択ステートメントに分離し、ステートメントの出現順序が原因で最後に実行するよう強制され、proc_warningsテーブルにのみ影響する削除ステートメントを動的に生成することによって実現されます。

デモはsql fiddleで入手できます。

この link は、サンプルデータを含むスキーマと、ivehicle_id=2に一致する行の簡単なクエリを示しています。削除されていないため、2行になります。

この link は同じスキーマ、サンプルデータを示していますが、値2をDeleteEntriesストアドプログラムに渡し、SPにproc_warningsivehicle_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行)、インデックスはロックされません。

これは、統計情報の変更による異なる実行プラン、非常に小さいデータセットによる非常に高速な実行による速すぎる観察ロックなど、多くのことを意味しますが、これらに限定されません。結合操作。

3
JM Hicks

READ_COMMITTEDがこの状況をどのように引き起こしているのかがわかります。

READ_COMMITTED は3つのことを可能にします:

  • READ_COMMITTED分離レベルを使用した、他のトランザクションによるコミットされた変更の可視性。
  • 繰り返し不可の読み取り:同じ検索を実行するトランザクションで、毎回異なる結果が得られる可能性があります。
  • ファントム:トランザクションでは、事前に表示されなかった場所に行が表示される場合があります。

トランザクションは以下との接触を維持する必要があるため、トランザクション自体に内部パラダイムが作成されます。

  • InnoDBバッファープール(コミットがまだフラッシュされていない間)
  • テーブルの主キー
  • たぶん
    • 二重書き込みバッファ
    • テーブルスペースを元に戻す
  • 図解

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_idlock_trx_idを除いて、残りのロックの説明は同じです。トランザクションは同じレベルの競争条件にあるため(同じトランザクション分離)、これは実際に発生するはずです

私を信じて、私は以前にこのような状況に対処したことがあります。これに関する私の過去の投稿は次のとおりです。

3
RolandoMySQLDBA

クエリと説明を確認しました。よくわかりませんが、問題は次のとおりだと直感しています。クエリを見てみましょう:

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;
2
newtover

方法を指定せずにトランザクションレベルを設定すると、次のトランザクションのみにコミット済み読み取りが適用されるため、(自動コミットを設定する)。これは、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;
2
Franck