MongoDB批量插入数据避免重复写入的高效解决方案
MongoDB批量插入需设置ordered:false以跳过重复键继续写入,错误信息位于writeErrors字段。唯一索引是防止重复的原子手段,需先清理已有重复项。批量写入总大小不能超16MB,建议每批≤5万条,并注意_id类型一致性,避免混用字符串与ObjectId。
很多开发者在进行 MongoDB 批量插入时都遇到过这样的问题:明明提交了 1 万条数据,结果只有一半成功写入,另一半悄无声息地丢失了。问题究竟出在哪里?其实不是代码写错了,而是对默认行为缺乏清晰的认识。

insertMany 默认会因重复键中断,必须显式关掉 ordered
默认情况下,ordered: true 意味着什么?只要某条文档触发了唯一索引冲突(比如经典的 E11000 duplicate key 错误),后续所有文档的插入就会立刻停止。这并非性能问题,而是 MongoDB 预设的行为——你看到的“只插入了一半”,实际上是主动中断的结果。
- 必须设置
{ ordered: false }才能让剩余文档继续执行 —— 这才是你期望的“跳过重复,继续插入”效果。 - 错误不会以异常形式抛出,而是存储在返回对象的
writeErrors字段中,需要手动检查。很多新手习惯用try/catch包裹整个insertMany来判断成败,却发现result.ok始终为1,即使有 99 条失败。因此,务必检查writeErrors才能准确判断实际写入结果。
唯一索引不是可选项,是防止重复的唯一原子手段
有人觉得可以在应用层通过“先查再插”或使用 upsert 来避免重复。但在并发场景下,两个请求同时查询未发现数据,就会同时插入,导致重复。只有存储引擎层的唯一索引,才是真正能原子性阻止重复的可靠手段。
- 创建索引时必须带上
{ unique: true },例如:db.users.createIndex({ email: 1 }, { unique: true })。不要省略这个参数,否则索引不会阻止重复写入。 - 如果数据中已经存在重复项,创建索引会直接失败。需要先清理:
db.users.aggregate([ { $group: { _id: "$email", count: { $sum: 1 } } }, { $match: { count: { $gt: 1 } } } ])找出重复记录,然后手动去重。 - 字段值中包含空格、大小写不一致,或者
null被多次插入,都会导致索引失效。此时需要配合collation指定排序规则,或者使用sparse: true过滤掉null值。
bulkWrite 比 insertMany 更适合混合场景,但别用 upsert 替代 insert
如果你的操作既包含插入新文档,又包含更新部分字段,bulkWrite 是更清晰的选择。但要注意,千万不要为了“跳过重复”而滥用 upsert: true。
upsert本质上是“查找 + 插入/更新”,每次操作多一次索引查找,性能远低于纯insertOne。如果只是要插入新数据,没必要使用 upsert。- 漏写
$set或 filter 写错,可能把整条文档覆盖成空对象 —— 这种 bug 很难排查。 - 真要跳过重复,就用
bulkWrite+insertOne+{ ordered: false },语义干净、开销最小。失败定位通过result.writeErrors[i].index,对应原始数组下标,而非插入后的顺序,注意区分。
批次大小不是按条数卡死,得按 BSON 总体积算
insertMany 提交的是一个 BSON 数组,总大小不能超过 16MB。单条文档 1KB 时,1.5 万条就达到上限;如果文档长短不一,硬按 10 万条切批,很可能遇到 Document too large for insert 错误。
- 预估体积用:
Buffer.byteLength(JSON.stringify(doc), 'utf8'),再加 20% 余量 —— 别省略这一步,生产环境里吃过亏的都懂。 - Node.js 里不要一次性
JSON.parse(fs.readFileSync(...))加载全部数据,建议用stream.Transform边读边 chunk,内存更友好。 - 驱动会自动按
maxWriteBatchSize(10 万)拆批,但这是上限而非建议值。生产环境推荐每批不超过 5 万条,既能保证吞吐量,又不会撑爆内存。
实际写入逻辑中最容易被忽略的,是 _id 字段类型的一致性 —— 字符串 "507f1f77bcf86cd799439011" 和 ObjectId("507f1f77bcf86cd799439011") 在索引中视为两个不同的值,看起来像是重复,实际上都能插入进去。所以,要么统一使用 ObjectId,要么统一使用字符串,不要混用。

