エラーの内容
ドキュメントを挿入または更新すると、MongoDBが以下のエラーを返します:
MongoServerError: E11000 duplicate key error collection: mydb.users index: email_1 dup key: { email: "john@example.com" }
このメッセージは非常に読みやすく、コレクション(mydb.users)、エラーが発生したインデックス(email_1)、そして重複した値("john@example.com")を明示しています。MongoDBはそのフィールドにユニークインデックスを設定しており、その値はすでに使用されています。
根本原因
ユニークインデックスは、インデックス対象フィールドで2つのドキュメントが同じ値を持たないことを保証します。E11000は以下の4つのよくある状況で発生します:
- コレクションにすでに存在するフィールド値を持つドキュメントを挿入しようとした場合。
upsertが既存ドキュメントを更新する代わりに、誤って新しいドキュメントを作成した場合。- すでに重複した値を持つコレクションにユニークインデックスを追加しようとした場合 — MongoDBはインデックスの作成を完全に拒否します。
- レースコンディション:2つの並行リクエストが数ミリ秒以内に同じ値を挿入しようとした場合。
修正1:重複データを見つけて削除する
まずコレクションの現状を把握しましょう。推測せずにクエリで確認します:
// 重複した値を持つすべてのドキュメントを検索
db.users.find({ email: "john@example.com" })
// コレクション全体の重複を集計
db.users.aggregate([
{ $group: { _id: "$email", count: { $sum: 1 } } },
{ $match: { count: { $gt: 1 } } }
])
重複しているドキュメントを特定したら、残したいものを1つ保持し、それ以外を削除します:
// _idで古いドキュメントを削除
db.users.deleteOne({ _id: ObjectId("64a1b2c3d4e5f6a7b8c9d0e1") })
修正2:upsertを正しく使用する
upsert: trueを設定すると、MongoDBはフィルタに一致するドキュメントを検索します。一致するものがない場合は新しいドキュメントを挿入します。問題はここにあります — フィルタにユニークフィールドが含まれていない場合、MongoDBは既存のドキュメントを更新する代わりに重複を作成してしまいます:
// 誤り — フィルタがユニークフィールドを対象としていない
db.users.updateOne(
{ name: "John" },
{ $set: { email: "john@example.com" } },
{ upsert: true }
)
// 正しい — ユニークフィールドでフィルタする
db.users.updateOne(
{ email: "john@example.com" },
{ $set: { name: "John" } },
{ upsert: true }
)
修正3:ユニークインデックスを削除または再作成する
ユニーク制約そのものが問題の場合もあります — スキーマが変更された、またはそもそも不要だった場合は削除します:
// インデックス一覧を確認して正しい名前を特定
db.users.getIndexes()
// ユニークインデックスを削除
db.users.dropIndex("email_1")
// 必要であればユニーク制約なしで再作成
db.users.createIndex({ email: 1 })
一意性は保持したいが既存データに重複がある場合は、先にデータをクリーンアップしてから再作成します:
// 重複を削除した後、ユニークインデックスを再作成
db.users.createIndex({ email: 1 }, { unique: true })
修正4:アプリケーションコードでエラーコード11000をキャッチする
ユーザー登録、フォーム送信、APIエンドポイントなど、ユーザーがデータを入力する場所では重複は日常的に発生します。例外をそのまま伝播させるのではなく、エラーをキャッチして明確なメッセージを返しましょう:
// Node.js / Mongoose
try {
await User.create({ email: "john@example.com" });
} catch (err) {
if (err.code === 11000) {
const field = Object.keys(err.keyValue)[0];
return res.status(409).json({ error: `${field} already exists` });
}
throw err;
}
# Python / PyMongo
from pymongo.errors import DuplicateKeyError
try:
collection.insert_one({ "email": "john@example.com" })
except DuplicateKeyError as e:
print(f"Duplicate entry: {e.details['keyValue']}")
修正5:レースコンディション — 並行インサート
2つのリクエストが同じミリ秒内に到着します。どちらも既存ドキュメントを確認し、見つからなかったため挿入を試みます。一方が成功し、もう一方がE11000を受け取ります。
ユニークインデックスは正しく機能しており、データを保護しています。アプリケーション層でエラーをハンドリングするか(上記のコード11000のキャッチを参照)、アトミックなfindOneAndUpdateとupsertを組み合わせて使用します:
db.users.findOneAndUpdate(
{ email: "john@example.com" },
{ $setOnInsert: { email: "john@example.com", createdAt: new Date() } },
{ upsert: true, returnDocument: "after" }
)
MongoDBはこれを単一のアトミック操作として実行します。既存のドキュメントを検索するか、新しいドキュメントを挿入するかのどちらかであり、両方が同時に行われることはありません。
予防策
- 一括インポート前にクエリを実行する — 大量データを読み込む前に、被害が発生してからではなく、事前に集計による重複チェックを実行しましょう。
- 一括インサートには
ordered: falseを使用する — 最初のエラーで処理を停止するのではなく、MongoDBが重複をスキップして残りの処理を継続します:
db.users.insertMany(docs, { ordered: false })
- **常にエラーコード11000をキャッチする** — ユニークインデックスを持つコレクションへの書き込みを行うすべてのルートまたはサービスで対応してください。
- **ユニークインデックスを追加する前にデータをクリーンアップする** — 重複チェックを実行し、余分なデータを削除してから、インデックスを作成します。
## 修正の確認
重複チェックを再度実行します。結果が空であれば成功です:
// 何も返らないはず db.users.aggregate([ { $group: { _id: "$email", count: { $sum: 1 } } }, { $match: { count: { $gt: 1 } } } ])
// インサートを再試行 — 今度は成功するはず db.users.insertOne({ email: "john@example.com", name: "John" }) // { acknowledged: true, insertedId: ObjectId("...") }

