MySQL 26.innodb_のデフォルト値は50で、設定を変更しなくても、先の値までまとめて確保してカウンターの保存回数を減らします。その代わり、INSERTをすべてcommitしたあとでも異常終了をきっかけに未使用の予約分が飛ばされ、欠番が発生する場合があります。
今回はMySQL 26.
カウンターを事前確保する理由
InnoDBはテーブルごとにメモリ上のカウンターを持ち、再起動後も採番を続けられるよう、その値を永続化しています。
MySQL 9.
MySQL 26.
公式マニュアルは、事前確保数を増やすと永続化の更新頻度が下がると説明しています。ソースコードの変数説明にも、設定値を大きくすると採番が速くなる旨が記載されています。INSERTが多い処理で、採番に伴う保存処理を効率化できるのが利点です。
事前確保するのはカウンターで、行の保存とcommitは通常どおりです。稼働中はメモリ上のカウンターを使うため、予約しただけでIDが飛ぶことはありません。
innodb_autoinc_preallocateについて
innodb_は実行中に変更できるグローバル変数で、0にすると事前確保を無効にできます。SMALLINTやTINYINTなど、2バイト以下の整数型は対象外です。設定値は次のSQLで確認できます。
SHOW GLOBAL VARIABLES LIKE 'innodb_autoinc_preallocate';
+----------------------------+-------+ | Variable_name | Value | +----------------------------+-------+ | innodb_autoinc_preallocate | 50 | +----------------------------+-------+
異常終了後は保存済みの予約値からカウンターを復元するため、未使用の値が飛ばされる場合があります。停止時にそのテーブルを使うトランザクションがなくても起こります。
正常終了では実際のカウンターを保存し、予約分による欠番は生じません。以下では、未commitのINSERTがない状態で違いを確認します。
テーブルの準備
auto_とauto_は1にしています。
デフォルトと同じ事前確保50をSET PERSISTで明示し、比較する条件ごとにテーブルを作り直します。
SET GLOBAL innodb_fast_shutdown = 1;
SET PERSIST innodb_autoinc_preallocate = 50;
SET SESSION auto_increment_increment = 1;
SET SESSION auto_increment_offset = 1;
SET SESSION autocommit = 1;
CREATE DATABASE IF NOT EXISTS autoinc_lab;
USE autoinc_lab;
DROP TABLE IF EXISTS autoinc_test;
CREATE TABLE autoinc_test (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
memo VARCHAR(32) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB;
INSERT INTO autoinc_test(memo) VALUES ('row-1');
INSERT INTO autoinc_test(memo) VALUES ('row-2');
INSERT INTO autoinc_test(memo) VALUES ('row-3');
SELECT COUNT(*) AS row_count, MAX(id) AS max_id FROM autoinc_test;
SELECT * FROM autoinc_test ORDER BY id;
最後の2つのSELECTの結果は次のとおりです。
+-----------+--------+ | row_count | max_id | +-----------+--------+ | 3 | 3 | +-----------+--------+ +----+-------+ | id | memo | +----+-------+ | 1 | row-1 | | 2 | row-2 | | 3 | row-3 | +----+-------+
IDは1、2、3になりました。autocommitが有効なので、各INSERTはcommit済みです。
正常終了した場合
次のSQLでmysqldを正常終了します。
SHUTDOWN;
起動し直して行数と最大IDを確認し、1行追加します。
USE autoinc_lab;
SET SESSION auto_increment_increment = 1;
SET SESSION auto_increment_offset = 1;
SET SESSION autocommit = 1;
SELECT COUNT(*) AS row_count, MAX(id) AS max_id FROM autoinc_test;
SET @max_id_before_insert = (SELECT MAX(id) FROM autoinc_test);
INSERT INTO autoinc_test(memo) VALUES ('after restart');
SELECT LAST_INSERT_ID() AS assigned_id,
LAST_INSERT_ID() - @max_id_before_insert - 1 AS skipped_ids;
SELECT * FROM autoinc_test ORDER BY id;
停止前の3行が残り、追加INSERTの結果は次のようになりました。
+-------------+-------------+ | assigned_id | skipped_ids | +-------------+-------------+ | 4 | 0 | +-------------+-------------+ +----+---------------+ | id | memo | +----+---------------+ | 1 | row-1 | | 2 | row-2 | | 3 | row-3 | | 4 | after restart | +----+---------------+
次のIDは4で、欠番はありません。innodb_を0、1、2にした場合も同じ結果でした。設定値2でも、SQLのSHUTDOWNとSIGKILLによる強制終了では結果が異なります。
異常終了した場合
テーブルを作り直して同じ3行をINSERTします。今度はmysqldへSIGKILLを送り、強制終了します。
再起動直後は行数3、最大ID3でしたが、追加INSERTの結果は変わりました。
+-------------+-------------+ | assigned_id | skipped_ids | +-------------+-------------+ | 52 | 48 | +-------------+-------------+
次のIDは52で、48個の値が使われないままになりました。SELECT * FROM autoinc_で見ると、元の3行は残っています。
+----+---------------+ | id | memo | +----+---------------+ | 1 | row-1 | | 2 | row-2 | | 3 | row-3 | | 52 | after restart | +----+---------------+
すべてcommit済みで、INSERTの失敗やrollbackがなくても、この結果になります。同じ条件で3回試し、すべて次のIDは52でした。
欠番数は予約範囲の使用状況で変わります。ID 51まで使ってから同じ強制終了を試すと、次のIDは52で欠番はありませんでした。デフォルト50でも、毎回50個の欠番ができるわけではありません。
マニュアルでは、設定値をN、auto_をYとすると、事前確保による欠番の大きさはmax(N, Y)以下とされています。他の操作による欠番まで、この範囲に収まるという意味ではありません。
事前確保を無効にした場合
設定を0にし、テーブルを作り直して同じ異常終了の手順を繰り返します。それまでの予約分を引き継がない条件で比較します。
SET PERSIST innodb_autoinc_preallocate = 0;
ID 1〜3をcommitしてから強制終了し、再起動後に1行追加すると、次の結果になりました。
+-------------+-------------+ | assigned_id | skipped_ids | +-------------+-------------+ | 4 | 0 | +-------------+-------------+
SELECT * FROM autoinc_の結果もID 1〜4になりました。
+----+---------------+ | id | memo | +----+---------------+ | 1 | row-1 | | 2 | row-2 | | 3 | row-3 | | 4 | after restart | +----+---------------+
次のIDは4で、事前確保による欠番はありません。9.
3行をINSERTした条件の結果をまとめると、次のようになります。
| MySQL | IDの型 | 事前確保 | 終了方法 | 再起動直後の 行数/最大ID |
次のID | 欠番数 |
|---|---|---|---|---|---|---|
| 26. |
INT | 50 | SHUTDOWN |
3 / 3 | 4 | 0 |
| 26. |
INT | 50 | SIGKILL | 3 / 3 | 52 | 48 |
| 26. |
INT | 0 | SIGKILL | 3 / 3 | 4 | 0 |
| 26. |
INT | 500 | SIGKILL | 3 / 3 | 502 | 498 |
| 26. |
SMALLINT | 50 | SIGKILL | 3 / 3 | 4 | 0 |
| 9. |
INT | ― | SHUTDOWN / SIGKILL |
3 / 3 | 4 | 0 |
設定500では次のIDが502に進み、対象外のSMALLINT UNSIGNEDでは4になりました。
AUTO_INCREMENTに連番を期待しない
AUTO_
行の識別子として使い、欠番を許容する設計なら、欠番自体は通常問題になりません。問題になるのは、番号の連続性を前提にした処理です。
| 連番を前提にした処理 | 今回の例で起きる問題 |
|---|---|
| 最大IDから登録件数を求める | 4行しかないのに52件と数えてしまう |
| IDを1ずつ取得し、行がなければ終了する | ID 4で終了し、ID 52を取りこぼす |
| 欠番を失敗やデータ消失として監視する | 48個の欠番を48件の失敗や消失と誤認する |
| 欠番のない業務番号として使う | 受付番号や帳票番号などの連番要件を満たせない |
件数は実際の行を数え、取得処理は次に存在する行を検索します。失敗の判断にはSQLのエラーや処理記録を使います。欠番を許さない業務番号が必要なら、識別用IDとは別に採番を設計します。26.
利用するときの注意点
設定値は、カウンター保存の負担と、異常終了時に飛ばされる値の大きさを考えて選びます。値を大きくするとメタデータ更新の頻度を減らせますが、未使用のまま飛ばされる範囲も大きくなり得ます。今回の3行の例でも、設定50では48個、500では498個の欠番が生じました。デフォルトの50を起点に、調整する場合は実際のINSERT負荷に近い条件で、処理件数や応答時間の変化を確認するとよいと思います。
事前確保を0にすると、今回の仕組みによる欠番は防げます。ただし、ソースコードの変数説明によると、26.mysql.を更新します。9.
事前確保によって整数型の上限が増えることもありません。異常終了で未使用の値が飛ばされると、行の件数以上にIDの範囲を消費します。大きな設定値を選ぶ場合は、IDの増加ペースと型の上限までの余裕も確認します。
まとめ
MySQL 26.
IDが飛んだ場合は、サーバーの終了方法と事前確保の設定も確認します。欠番の数をデータ消失の件数とはみなさず、実際に保存された行と合わせて判断するべきでしょう。