何が起きたのか
マイグレーションスクリプト、データインポート、手動クエリなど、バッチINSERTを実行していたところ、MySQLがこのエラーを返しました:
ERROR 1062 (23000): Duplicate entry '42' for key 'PRIMARY'
引用符の中の数字は、テーブルにすでに存在するIDです。MySQLは主キーがすでに使用されている行の挿入を拒否し、その行(またはトランザクション内にいる場合はトランザクション全体)をロールバックします。
ほぼ毎回、3つの状況がこのエラーを引き起こします。1つ目は、既存データと衝突する明示的なID値を持つ行を挿入している場合。2つ目は、AUTO_INCREMENTカウンターが同期ずれを起こしている場合 — データベースの復元後や手動でのID編集後によく発生します。3つ目は、既存行の存在を確認せずに盲目的に挿入するスクリプトを実行している場合です。
デバッグ手順
1. どのIDが競合しているかを特定する
エラーメッセージに直接記載されています — 引用符内の数字が重複した値です。全体像を把握したい一括挿入の場合:
-- そのIDのテーブル内の既存データを確認する
SELECT id FROM your_table WHERE id = 42;
-- 現在のAUTO_INCREMENT値を確認する
SHOW TABLE STATUS LIKE 'your_table'\G
出力のAuto_incrementを確認してください。最大の既存IDより低い場合、それが問題の原因です。
2. テーブル内の実際の最大IDを調べる
SELECT MAX(id) FROM your_table;
上記のAuto_increment値と比較してください。たとえばMAX(id)が500なのにAuto_incrementが100を示している場合 — MySQLは100からIDを割り当て始め、即座に既存行と衝突します。
3. 明示的なIDを挿入していないか確認する
INSERTが以下のような形の場合:
INSERT INTO users (id, name, email) VALUES (42, 'Alice', 'alice@example.com');
IDを手動で指定しています。行42が存在すれば失敗します。これはマイグレーションスクリプトやデータインポートで、IDがソースデータベースから直接来る場合によくあるパターンです。
解決策
オプション1:AUTO_INCREMENTを正しい値にリセットする
データベースを復元した、切り詰めて再シードした、または手動でIDを変更した場合、auto_incrementカウンターはおそらく古い状態になっています。
-- 明示的に設定する
ALTER TABLE your_table AUTO_INCREMENT = 501;
-- またはMySQLに自動で決めさせる:
ALTER TABLE your_table AUTO_INCREMENT = 1;
-- 指定した値が低すぎる場合、MySQLがMAX(id)+1に設定する
これ以降、明示的なIDなしの新しい挿入は安全な値から始まります。
オプション2:INSERT IGNOREで重複をスキップする
既存の行を再挿入する可能性があるスクリプトを実行しており、単に静かにスキップしたい場合:
INSERT IGNORE INTO users (id, name, email)
VALUES (42, 'Alice', 'alice@example.com');
重複行は静かにスキップされます。エラーなし、ロールバックなし。スキップが正しい動作である場合のみ使用してください — 既存行を更新したい場合には使用しないでください。
オプション3:INSERT ... ON DUPLICATE KEY UPDATEを使う
既存行が存在する場合に更新したい場合、それがアップサートパターンです:
INSERT INTO users (id, name, email)
VALUES (42, 'Alice', 'alice@example.com')
ON DUPLICATE KEY UPDATE
name = VALUES(name),
email = VALUES(email);
同期ジョブや冪等スクリプトでは、これをデフォルトにしてください。1つのクエリで新規行と更新の両方を処理します — 条件分岐ロジックは不要です。
オプション4:REPLACE INTOを使う(注意が必要)
REPLACE INTOは競合している行を最初に削除し、新しい行を挿入します。機能しますが、注意が必要です:INSERTにないカラムをリセットし、DELETEトリガーが発火し、エッジケースでは内部行IDが変更される可能性があります。
REPLACE INTO users (id, name, email)
VALUES (42, 'Alice', 'alice@example.com');
これらの副作用を確実に理解している場合のみ使用してください。
オプション5:INSERTから明示的なIDを削除する
特定のID値が実際に必要ない場合は、INSERTからidを削除してMySQLに割り当てを任せましょう:
-- 修正前(id=42が存在する場合は失敗する)
INSERT INTO users (id, name, email) VALUES (42, 'Alice', 'alice@example.com');
-- 修正後(MySQLが次の利用可能なidを割り当てる)
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
修正を確認する
-- 1. AUTO_INCREMENTが正常か確認する
SHOW TABLE STATUS LIKE 'your_table'\G
-- Auto_incrementはMAX(id) + 1以上であるべき
-- 2. 明示的なIDなしでテスト行を挿入する
INSERT INTO users (name, email) VALUES ('Test', 'test@example.com');
SELECT LAST_INSERT_ID();
-- 現在の最大値より大きい新しいIDが返されるはず
-- 3. 元の失敗したクエリを再実行する
-- これで成功するはず
今後のマイグレーションでの防止策
これらの習慣を身につけることで、このエラーはほとんど発生しなくなります:
- データベースダンプを復元した後、すべてのauto-incrementテーブルで
ALTER TABLE t AUTO_INCREMENT = 1を実行してください。MySQLが自動的にmax+1に修正します。 - 明示的なIDを持つシードデータを挿入するマイグレーションスクリプトでは、ガードを追加してください:
INSERT INTO ... SELECT ... WHERE NOT EXISTS (...)。 - 外部ソースからデータを取得する同期ジョブでは、
ON DUPLICATE KEY UPDATEをデフォルトにしてください — 1つのクエリで新規行と更新の両方をカバーします。 - ORMを使用している場合、すでに永続化されたオブジェクトを再挿入していないか確認してください。saveまたはcreateを呼び出す前に、エンティティにすでにIDが割り当てられているかどうかを確認してください。
クイックリファレンス
-- 競合している行を見つける
SELECT * FROM your_table WHERE id = <duplicate_value>;
-- AUTO_INCREMENTを確認して修正する
SHOW TABLE STATUS LIKE 'your_table'\G
ALTER TABLE your_table AUTO_INCREMENT = 1;
-- 一括挿入時に重複をスキップする
INSERT IGNORE INTO ...
-- アップサートパターン
INSERT INTO ... ON DUPLICATE KEY UPDATE ...;

