MongoDB: 插入文档:insertOne与insertMany详解
最后更新:2026-08-26
文档插入是 MongoDB 数据写入的第一步——掌握 insertOne 与 insertMany 是数据操作的基础。
本课程深入学习插入文档的各种方法、错误处理、性能优化,以及 Write Concern 写入关注机制。
1. 你将学到
- insertOne 与 insertMany 的核心用法
- ordered 选项与批量插入行为
- Write Concern 写入关注(w、j、wtimeout)
- _id 冲突的处理策略
- 批量插入的性能优化
- 插入日期、ObjectId 等特殊类型
- 常见插入错误排查
2. 一个数据工程师的真实故事
(1) 痛点:批量导入 100 万条数据经常失败
Alice 是一家电商公司的数据工程师,需要将 MySQL 的 100 万条产品数据迁移到 MongoDB:
"我用
insertMany导入 100 万条产品数据,第 50 万条因为 _id 重复整个导入失败,浪费了 4 小时。要么全成功要么全失败,这对增量迁移简直是灾难。"
她面临的问题:
| 问题 | 影响 |
|---|---|
| ordered: true 默认 | 中间一条失败,整批失败 |
| 缺少 Write Concern | 服务器宕机导致数据丢失 |
| _id 冲突 | MySQL 自增 ID 重复导致失败 |
| 大批量插入 | 100 万次单独插入,耗时 1 小时 |
(2) MongoDB + bulkWrite 的解法
// === 使用 bulkWrite + ordered: false 解决部分失败问题 ===
const products = [...]; // 100 万条产品数据
const BATCH_SIZE = 1000;
for (let i = 0; i < products.length; i += BATCH_SIZE) {
const batch = products.slice(i, i + BATCH_SIZE);
try {
await Product.bulkWrite(
batch.map(doc => ({
insertOne: { document: doc }
})),
{ ordered: false } // 允许部分失败,继续执行
);
} catch (err) {
console.error(`Batch ${i / BATCH_SIZE} 失败:${err.writeErrors?.length} 条`);
}
}
// === 使用 Write Concern 确保数据持久化 ===
await Product.bulkWrite(
batch.map(doc => ({ insertOne: { document: doc } })),
{
ordered: false,
writeConcern: { w: 'majority', j: true, wtimeout: 5000 }
}
);
(3) 收益
| 维度 | 单条插入 | 批量插入 + ordered: false |
|---|---|---|
| 性能 | 100 万条 ~60 分钟 | 100 万条 ~3 分钟 |
| 容错性 | 单条失败丢失 | 部分失败继续 |
| 数据安全 | 易丢失 | Write Concern 持久化 |
| 代码复杂度 | 简单 | 中等 |
3. insertOne 插入单个文档
概念说明:insertOne 是 MongoDB 最基础的写入方法,向集合中插入一条文档。每条文档在 MongoDB 中以 BSON 格式存储,自动获得唯一 _id 主键。与关系型数据库的 INSERT INTO 不同,insertOne 无需预定义表结构,文档可以包含任意字段组合。
工作原理:当客户端发起 insertOne 请求时,MongoDB 服务端执行以下流程——校验 BSON 格式 → 检查 _id 唯一性 → 写入 WiredTiger 存储引擎 → 应用 Write Concern 确认 → 返回 insertedId。整个写入过程对单文档是原子的。
sequenceDiagram
participant App as 应用程序
participant Mongod as MongoDB 服务端
participant WT as WiredTiger 引擎
App->>Mongod: insertOne({ doc })
Mongod->>Mongod: 校验 BSON 格式
Mongod->>Mongod: 检查 _id 唯一索引
alt _id 冲突
Mongod-->>App: E11000 duplicate key error
else _id 唯一
Mongod->>WT: 写入文档 + 更新索引
WT-->>Mongod: 确认写入
Mongod-->>App: { acknowledged: true, insertedId }
end
| 参数 | 类型 | 说明 |
|---|---|---|
document |
Document | 要插入的文档(必填) |
writeConcern |
Document | 写入确认级别(可选) |
| 适用场景 | 不适用场景 |
|---|---|
| 单条数据创建(用户注册) | 大批量数据导入 |
需要获取 insertedId |
超过 1000 条的批量写入 |
| 带复杂嵌套的文档 | 重复数据去重导入 |
(1) 基本语法
// === insertOne 基本用法 ===
db.products.insertOne({
sku: "PHONE-001",
title: "Smartphone X",
price: NumberDecimal("599.99"),
category: "Electronics",
stock: 50,
createdAt: new Date()
});
// 返回结果:
// {
// acknowledged: true,
// insertedId: ObjectId('507f1f77bcf86cd799439011')
// }
要点解析:
acknowledged: true表示写入操作已被 MongoDB 服务端确认(受 Write Concern 影响)- 如果
writeConcern: { w: 0 },则acknowledged为false,且不返回insertedId insertedId的值取决于是否手动指定_id——未指定则自动生成 ObjectId
(2) 返回值解析
概念说明:insertOne 的返回值包含两个关键字段——acknowledged 和 insertedId。acknowledged 为 true 表示写入操作已被 MongoDB 服务端确认(受 Write Concern 影响,如果 w: 0 则为 false)。insertedId 是插入文档的 _id 值,无论 _id 是自动生成还是手动指定,都会返回。
| 返回字段 | 类型 | 说明 | 注意事项 |
|---|---|---|---|
acknowledged |
Boolean | 写入是否已确认 | w: 0 时为 false |
insertedId |
ObjectId/任意 | 插入文档的 _id | 手动指定时返回指定值 |
const result = db.products.insertOne({
sku: "TEST-001",
title: "Test Product"
});
print(result.acknowledged); // true(写入已确认)
print(result.insertedId); // ObjectId('507f1f77bcf86cd799439012')
| 字段 | 类型 | 说明 |
|---|---|---|
acknowledged |
boolean | true 表示写入已确认 |
insertedId |
ObjectId | 插入文档的 _id |
(3) _id 自动生成
概念说明:_id 是 MongoDB 文档的主键,默认类型为 ObjectId(12 字节二进制)。如果不手动指定 _id,MongoDB 驱动程序会在客户端自动生成,确保在写入服务端之前就已唯一。这一设计与 MySQL 的自增 ID 不同——ObjectId 不依赖中心化计数器,天然支持分布式环境。
工作原理:ObjectId 由 4 字节时间戳 + 5 字节随机值(机器+进程)+ 3 字节递增计数器组成。时间戳部分使其天然按插入时间排序,随机值保证跨进程唯一性,计数器保证同一秒内唯一。
graph LR
A[客户端生成 ObjectId] --> B[时间戳 4B<br/>插入时间]
A --> C[随机值 5B<br/>机器+进程唯一]
A --> D[计数器 3B<br/>同一秒内递增]
B --> E[全局唯一<br/>天然有序<br/>可提取时间]
| _id 策略 | 示例 | 适用场景 | 排序性 |
|---|---|---|---|
| 自动 ObjectId | ObjectId("...") |
通用场景(默认) | ✅ 按时间排序 |
| 字符串业务键 | "ORDER-2026-001" |
订单号、SKU | 取决于格式 |
| 数字自增 | NumberInt(1) |
遗留系统迁移 | ✅ 按数字排序 |
| 时间戳 | NumberLong(1700000000) |
时序数据 | ✅ 按时间排序 |
| UUID | UUID("...") |
跨系统唯一 | ❌ 无序 |
// === 不指定 _id(自动生成 ObjectId)===
db.users.insertOne({
name: "Alice",
email: "alice@example.com"
});
// 自动生成 _id: ObjectId('507f1f77bcf86cd799439011')
// === 手动指定 _id ===
db.users.insertOne({
_id: "user_001", // 字符串 ID
name: "Alice"
});
db.users.insertOne({
_id: ObjectId(), // 手动生成 ObjectId
name: "Bob"
});
db.users.insertOne({
_id: NumberLong(1700000000000), // 时间戳作为 ID
name: "Charlie"
});
(4) _id 唯一性
概念说明:MongoDB 为每个集合的 _id 字段自动创建唯一索引,这是数据完整性的基础保障。_id 唯一索引的特殊之处在于它不可删除——即使执行 dropIndexes(),_id 索引仍然保留。当插入重复 _id 的文档时,MongoDB 抛出 E11000 duplicate key error,整个插入操作回滚。
使用场景:在数据迁移和批量导入场景中,_id 冲突是最常见的错误来源。理解如何预防和处理冲突是生产环境稳定运行的关键。推荐的预防策略是:导入前先查询已存在的 _id 集合,或使用 upsert 模式替代 insertOne。
| 场景 | 冲突原因 | 推荐处理策略 |
|---|---|---|
| MySQL → MongoDB 迁移 | 自增 ID 与已有数据冲突 | 去掉旧 _id,让 MongoDB 生成 |
| 多源数据合并 | 不同数据源有相同业务键 | 加前缀:sourceA_ORDER-001 |
| 增量同步 | 源数据已存在于目标 | updateOne + upsert |
| 批量导入 | CSV/JSON 含重复行 | ordered: false 跳过重复 |
// === _id 重复的错误处理 ===
try {
db.users.insertOne({
_id: "user_001", // 已存在
name: "Alice Duplicate"
});
} catch (err) {
// E11000 duplicate key error collection: shopdb.users index: _id_
print("❌ _id 已存在:" + err.message);
}
// === 使用 upsert 处理重复 ===
db.users.updateOne(
{ _id: "user_001" },
{ $set: { name: "Alice Updated" } },
{ upsert: true } // 不存在则插入,存在则更新
);
▶ 示例 1:完整的 insertOne 用法
// === 插入不同类型的字段 ===
db.products.insertOne({
// 字符串
sku: "PHONE-X-256-BLK",
title: "Smartphone X 256GB Black",
// 数值类型
price: NumberDecimal("599.99"), // Decimal128(精确)
stock: NumberInt(50), // Int32
viewCount: NumberLong(1000000), // Long
// 布尔
isActive: true,
isFeatured: false,
// 日期
createdAt: new Date(),
releaseDate: ISODate("2026-01-01"),
// 数组
tags: ["5g", "amoled", "fast-charging"],
colors: ["Black", "White", "Blue"],
// 嵌套文档
specs: {
screen: "6.5 inch OLED",
battery: "4500mAh",
camera: "108MP"
},
// 二进制
thumbnail: BinData(0, "iVBORw0KGgoAAAANSUhEUgAA..."),
// Null
discount: null
});
输出:
// 执行成功
▶ 示例 2:insertOne 带不同 _id 策略
// === 策略 1:自动 ObjectId(默认)===
const r1 = db.users.insertOne({ name: "Alice", email: "alice@example.com" });
print(`Auto ObjectId: ${r1.insertedId}`);
// === 策略 2:字符串业务键 ===
const r2 = db.orders.insertOne({
_id: "ORD-20260701-0001",
total: NumberDecimal("599.99"),
status: "pending"
});
print(`Business key: ${r2.insertedId}`);
// === 策略 3:嵌套文档 + 数组 ===
const r3 = db.products.insertOne({
_id: ObjectId(),
sku: "PHONE-X-256-BLK",
specs: { screen: "6.5 inch OLED", battery: "4500mAh" },
tags: ["5g", "amoled"],
price: NumberDecimal("599.99")
});
print(`Nested doc: ${r3.insertedId}`);
输出:Auto ObjectId: ObjectId('...') | Business key: ORD-20260701-0001 | Nested doc: ObjectId('...')
4. insertMany 批量插入
概念说明:insertMany 一次插入多条文档,是批量数据写入的核心方法。相比逐条 insertOne,insertMany 将多条文档合并为一次网络请求发送到服务端,大幅减少网络往返开销,性能可提升 10-100 倍。
工作原理:insertMany 接收文档数组,按 ordered 选项决定执行策略。ordered: true(默认)按顺序逐条插入,遇到错误立即停止;ordered: false 允许并行插入,跳过失败项继续执行。两种策略对性能和数据完整性影响显著。
graph TB
A[insertMany<br/>1000 条文档] --> B{ordered 选项}
B -->|ordered: true| C[顺序插入<br/>第1条→第2条→...<br/>遇错停止]
B -->|ordered: false| D[并行插入<br/>多条同时写入<br/>跳过失败项]
C --> C1[性能:中等<br/>一致性:强]
D --> D1[性能:更高<br/>一致性:弱]
style D fill:#d4edda
| 参数 | 类型 | 说明 |
|---|---|---|
documents |
Array | 文档数组(必填,至少 1 条) |
ordered |
Boolean | true 顺序执行(默认),false 并行执行 |
writeConcern |
Document | 写入确认级别 |
| 适用场景 | 不适用场景 |
|---|---|
| 数据迁移、批量导入 | 单条文档插入 |
| 测试数据生成 | 需要严格事务顺序的写入 |
| 日志批量写入 | 文档间有强依赖关系 |
(1) 基本语法
// === insertMany 基本用法 ===
db.products.insertMany([
{ sku: "PHONE-001", title: "Phone A", price: 599.99 },
{ sku: "PHONE-002", title: "Phone B", price: 699.99 },
{ sku: "PHONE-003", title: "Phone C", price: 799.99 }
]);
// 返回结果:
// {
// acknowledged: true,
// insertedIds: {
// '0': ObjectId('507f1f77bcf86cd799439011'),
// '1': ObjectId('507f1f77bcf86cd799439012'),
// '2': ObjectId('507f1f77bcf86cd799439013')
// },
// insertedCount: 3
// }
(2) ordered 选项(关键!)
概念说明:ordered 是 insertMany 最关键的选项。它决定了 MongoDB 如何处理批量写入中的错误——是立即中止还是跳过继续。理解 ordered 对生产环境的数据导入至关重要。
使用场景:数据迁移和增量同步场景推荐 ordered: false,因为源数据可能包含重复 _id,跳过重复继续导入比整批失败更合理。金融事务场景推荐 ordered: true,保证操作的严格顺序性。
// === ordered: true(默认)— 中间失败则停止 ===
db.products.insertMany([
{ _id: 1, sku: "A" },
{ _id: 2, sku: "B" },
{ _id: 1, sku: "C" }, // ❌ _id 冲突
{ _id: 4, sku: "D" } // ⚠️ 不会插入(前面失败)
]);
// 错误:E11000 duplicate key error
// 实际插入:A, B(2 条),C 和 D 未插入
// === ordered: false — 跳过失败,继续执行 ===
db.products.insertMany([
{ _id: 1, sku: "A" },
{ _id: 2, sku: "B" },
{ _id: 1, sku: "C" }, // ❌ _id 冲突
{ _id: 4, sku: "D" } // ✅ 仍然插入
], { ordered: false });
// 错误信息包含所有失败的文档索引:
// BulkWriteError: 1 document(s) failed
// writeErrors: [
// { index: 2, code: 11000, errmsg: 'duplicate key' }
// ]
// 实际插入:A, B, D(3 条),C 未插入
(3) ordered 选项对比
| 维度 | ordered: true | ordered: false |
|---|---|---|
| 失败行为 | 整个批量失败 | 跳过失败继续 |
| 性能 | 中等 | 更快(并行) |
| 适用场景 | 强一致性(如转账) | 增量导入、日志 |
| 错误信息 | 第一条失败 | 全部失败详情 |
▶ 示例 3:完整的批量插入实战
// === 电商产品批量插入示例 ===
const products = [
{ sku: "LAPTOP-001", title: "Laptop Pro", price: NumberDecimal("1299.99"), category: "Electronics", stock: 20 },
{ sku: "LAPTOP-002", title: "Laptop Air", price: NumberDecimal("999.99"), category: "Electronics", stock: 30 },
{ sku: "PHONE-001", title: "Smartphone X", price: NumberDecimal("599.99"), category: "Electronics", stock: 50 },
{ sku: "BOOK-001", title: "JavaScript Guide", price: NumberDecimal("29.99"), category: "Books", stock: 200 },
{ sku: "BOOK-002", title: "MongoDB Tutorial", price: NumberDecimal("34.99"), category: "Books", stock: 150 }
];
// === 默认模式(有序)===
try {
const result = db.products.insertMany(products);
print(`✅ 插入 ${result.insertedCount} 条产品`);
} catch (err) {
print(`❌ 批量失败:${err.message}`);
}
// === 容错模式(无序)===
try {
const result = db.products.insertMany(products, { ordered: false });
print(`✅ 插入 ${result.insertedCount} 条产品`);
} catch (err) {
print(`⚠️ 部分失败:成功 ${err.result.insertedCount} 条,失败 ${err.writeErrors.length} 条`);
err.writeErrors.forEach(e => print(` 失败索引 ${e.index}: ${e.errmsg}`));
}
输出:
// 执行成功
5. Write Concern 写入关注
概念说明:Write Concern 是 MongoDB 的写入安全机制,定义了"一次写入操作何时算成功"。它控制写入操作必须在多少个副本节点确认后才返回客户端。这是数据持久性与写入性能之间的核心权衡——确认级别越高越安全,但延迟也越大。
工作原理:在副本集架构中,写入操作先到达 Primary 节点,再异步复制到 Secondary 节点。Write Concern 的 w 参数决定需要等待多少个节点确认。w: 1 只等 Primary 确认(最快但有丢失风险),w: "majority" 等待大多数节点确认(推荐生产环境),j: true 确保数据已写入磁盘日志(journal)。
sequenceDiagram
participant App as 客户端
participant P as Primary
participant S1 as Secondary 1
participant S2 as Secondary 2
App->>P: insertOne({ doc }, { w: "majority" })
P->>P: 写入内存 + Journal
P->>S1: 复制 oplog
P->>S2: 复制 oplog
S1-->>P: 确认写入
S2-->>P: 确认写入
Note over P: majority 达成(2/3 节点)
P-->>App: { acknowledged: true }
| 维度 | w: 0 | w: 1 | w: majority | w: majority + j: true |
|---|---|---|---|---|
| 确认节点数 | 不等待 | Primary | 大多数节点 | 大多数节点 + 磁盘 |
| 性能 | 最快 | 快 | 中等 | 较慢 |
| 数据安全 | 可能丢失 | Primary宕机可丢 | 几乎不丢 | 最安全 |
| 推荐场景 | 日志 | 开发 | 生产 | 金融 |
(1) 什么是 Write Concern?
Write Concern 描述写入操作的成功确认级别,决定数据何时算"已保存"。
graph LR
A[客户端] -->|insertOne| B[mongod 接收]
B --> C{Write Concern 配置}
C -->|w: 1| D[Primary 写入即返回]
C -->|w: majority| E[大多数节点确认后返回]
C -->|j: true| F[写入磁盘后才返回]
style E fill:#d4edda
style F fill:#d4edda
(2) Write Concern 参数
| 参数 | 取值 | 说明 |
|---|---|---|
w |
0 / 1 / "majority" / 数字 |
写入确认的节点数 |
j |
true / false |
是否写入磁盘 journal |
wtimeout |
毫秒数 | 超时时间(默认无限等待) |
(3) Write Concern 级别对比
// === w: 0 — 不等待确认(最快,可能丢失)===
db.products.insertOne(
{ sku: "TEST-001", title: "Test" },
{ writeConcern: { w: 0 } }
);
// 立即返回,不保证写入成功
// === w: 1 — Primary 节点确认(默认)===
db.products.insertOne(
{ sku: "TEST-002", title: "Test" },
{ writeConcern: { w: 1 } }
);
// Primary 写入即返回
// === w: "majority" — 大多数节点确认(最安全)===
db.products.insertOne(
{ sku: "TEST-003", title: "Test" },
{ writeConcern: { w: "majority", j: true, wtimeout: 5000 } }
);
// 副本集中大多数节点写入磁盘才返回(推荐生产环境)
(4) Write Concern 配置对比
| 级别 | 性能 | 数据安全 | 适用场景 |
|---|---|---|---|
w: 0 |
⚡⚡⚡ 极快 | ❌ 易丢失 | 日志、临时数据 |
w: 1 |
⚡⚡ 快 | ⚠️ 可能丢失 | 单机开发 |
w: majority |
⚡ 中等 | ✅ 几乎不丢失 | 生产环境推荐 |
w: majority, j: true |
⚠️ 较慢 | ✅✅ 最安全 | 金融、关键数据 |
▶ 示例 4:生产级 Write Concern 配置
// === 集群级别设置(推荐)===
db.adminCommand({
setDefaultRWConcern: 1,
defaultWriteConcern: { w: "majority", j: true, wtimeout: 10000 },
defaultReadConcern: { level: "majority" }
});
// === 单次写入指定 ===
db.orders.insertOne(
{ userId: "user_001", total: 599.99, items: [...] },
{ writeConcern: { w: "majority", j: true, wtimeout: 5000 } }
);
// === mongoose 中设置 ===
const OrderSchema = new mongoose.Schema({
userId: String,
total: mongoose.Schema.Types.Decimal128,
items: Array
}, {
writeConcern: { w: 'majority', j: true, wtimeout: 5000 }
});
输出:
// mongoose 操作成功执行
// 数据库查询/更新结果
6. _id 冲突处理策略
概念说明:_id 是 MongoDB 文档的唯一标识,每个集合的 _id 字段自动创建唯一索引。当插入的文档 _id 与已有文档重复时,MongoDB 抛出 E11000 duplicate key error。在数据迁移、批量导入、多源合并等场景中,_id 冲突是最常见的问题之一。
工作原理:MongoDB 在写入文档前,首先检查 _id 字段是否违反唯一索引约束。如果冲突,整个写入操作回滚(单文档原子性),并返回错误码 11000。理解不同冲突处理策略的区别,对生产环境的数据完整性至关重要。
graph TB
A[_id 冲突 E11000] --> B[策略选择]
B --> C[忽略重复<br/>ordered: false]
B --> D[覆盖旧值<br/>replaceOne + upsert]
B --> E[部分更新<br/>updateOne + upsert]
B --> F[重新生成 _id<br/>去掉 _id 字段]
B --> G[重试机制<br/>应用层重试]
style E fill:#d4edda
| 策略 | 语法 | 数据完整性 | 适用场景 |
|---|---|---|---|
| 跳过重复 | ordered: false |
保留旧数据 | 增量导入、日志 |
| 覆盖旧值 | replaceOne + upsert |
替换为新数据 | 全量同步 |
| 部分更新 | updateOne + upsert |
合并新旧数据 | 增量更新字段 |
| 忽略 _id | 删除 _id 字段 |
全部插入(新 _id) | 去重导入 |
| 重试机制 | 应用层重试 | 取决于新 _id | 临时性冲突 |
(1) 错误现象
// === _id 重复的错误 ===
db.users.insertOne({ _id: 1, name: "Alice" });
// { acknowledged: true, insertedId: 1 }
db.users.insertOne({ _id: 1, name: "Bob Duplicate" });
// E11000 duplicate key error collection: shopdb.users index: _id_ dup key: { _id: 1 }
(2) 5 种处理策略
graph TB
A[_id 冲突] --> B[策略 1<br/>跳过重复]
A --> C[策略 2<br/>覆盖旧值]
A --> D[策略 3<br/>upsert 自动选择]
A --> E[策略 4<br/>忽略 _id 字段]
A --> F[策略 5<br/>重试机制]
style D fill:#d4edda
(3) 策略实现
// === 策略 1:使用 ordered: false 跳过重复 ===
try {
db.users.insertMany(
[{ _id: 1, name: "Alice" }, { _id: 2, name: "Bob" }, { _id: 1, name: "Dup" }],
{ ordered: false }
);
} catch (err) {
print(`跳过 ${err.writeErrors.length} 条重复`);
}
// === 策略 2:使用 replaceOne 覆盖 ===
db.users.replaceOne(
{ _id: 1 },
{ _id: 1, name: "Alice Updated", updatedAt: new Date() },
{ upsert: true }
);
// === 策略 3:使用 updateOne + upsert ===
db.users.updateOne(
{ _id: 1 },
{ $set: { name: "Alice", email: "alice@example.com" } },
{ upsert: true } // 不存在则插入,存在则更新
);
// === 策略 4:插入时让 MongoDB 自动生成 _id ===
const docs = externalData.map(d => {
const { _id, ...rest } = d; // 解构去掉 _id
return rest; // 让 MongoDB 自动生成 _id
});
db.users.insertMany(docs);
// === 策略 5:重试机制(应用层)===
async function insertWithRetry(doc, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await db.collection('users').insertOne(doc);
} catch (err) {
if (err.code === 11000 && i < maxRetries - 1) {
// 生成新的 _id 重试
doc._id = new ObjectId();
continue;
}
throw err;
}
}
}
▶ 示例 5:批量导入 + 去重策略
// === 场景:导入 CSV 数据,部分 _id 已存在 ===
const csvData = [
{ _id: "USER-001", name: "Alice", email: "alice@example.com" },
{ _id: "USER-002", name: "Bob", email: "bob@example.com" },
{ _id: "USER-001", name: "Alice Duplicate", email: "alice2@example.com" },
{ _id: "USER-003", name: "Charlie", email: "charlie@example.com" }
];
// === 方案 A:忽略重复,仅插入新数据 ===
const insertedIds = [];
const duplicates = [];
csvData.forEach(doc => {
try {
const result = db.users.insertOne(doc);
insertedIds.push(result.insertedId);
} catch (err) {
if (err.code === 11000) {
duplicates.push(doc._id);
} else {
throw err;
}
}
});
print(`✅ 插入 ${insertedIds.length} 条新数据`);
print(`⚠️ 跳过 ${duplicates.length} 条重复:${duplicates.join(', ')}`);
// === 方案 B:覆盖重复,更新已有数据 ===
db.users.bulkWrite(
csvData.map(doc => ({
replaceOne: {
filter: { _id: doc._id },
replacement: doc,
upsert: true
}
})),
{ ordered: false }
);
输出:
// 执行成功
7. 批量插入性能优化
概念说明:批量插入的性能瓶颈主要来自三个方面:网络往返开销、索引更新开销和磁盘 I/O 开销。理解这三个瓶颈并逐一优化,可以将 100 万条数据的导入时间从 60 分钟缩短到 3 分钟以内。
工作原理:逐条 insertOne 每次都触发一次网络请求 + 一次索引更新 + 一次磁盘写入。insertMany 将多条文档合并为一次网络请求,减少 2/3 的开销。bulkWrite 进一步支持混合操作类型(insert+update+delete),在单次请求中完成。导入前临时删除非必要索引,可再提升 5-10 倍性能。
graph LR
A[100万条数据] --> B[逐条 insertOne<br/>~60 分钟<br/>100万次网络请求]
A --> C[insertMany 1000/批<br/>~5 分钟<br/>1000次网络请求]
A --> D[bulkWrite + dropIndexes<br/>~3 分钟<br/>1000次请求+无索引更新]
style D fill:#d4edda
| 优化策略 | 性能提升 | 风险 | 推荐场景 |
|---|---|---|---|
insertMany 替代 insertOne |
10-100x | 无 | 所有批量写入 |
ordered: false |
1.5-3x | 可能跳过失败项 | 容错导入 |
| 临时删除索引 | 5-10x | 导入完需重建 | 初始化导入 |
bulkWrite 替代 insertMany |
1.2-1.5x | 无 | 混合操作 |
w: 0(不等待确认) |
2-5x | 数据可能丢失 | 临时数据、日志 |
(1) 性能对比
graph LR
A[插入 100 万条数据] --> B[逐条插入<br/>~60 分钟]
A --> C[insertMany 1000/批<br/>~5 分钟]
A --> D[bulkWrite 1000/批<br/>~3 分钟]
style D fill:#d4edda
(2) 优化策略
// === 优化 1:合理的批量大小 ===
const BATCH_SIZE = 1000; // 推荐 500-5000
for (let i = 0; i < data.length; i += BATCH_SIZE) {
const batch = data.slice(i, i + BATCH_SIZE);
db.collection.insertMany(batch, { ordered: false });
}
// === 优化 2:使用 bulkWrite 替代 insertMany ===
await Collection.bulkWrite(
data.map(doc => ({ insertOne: { document: doc } })),
{ ordered: false }
);
// === 优化 3:禁用索引(导入期间)===
// ⚠️ 谨慎使用:导入完记得重建索引
db.products.dropIndexes();
db.products.insertMany(data);
// 重建索引
db.products.createIndex({ sku: 1 }, { unique: true });
// === 优化 4:使用 Write Concern 0(极快但不安全)===
db.products.insertMany(data, { writeConcern: { w: 0 } });
// ⚠️ 仅用于临时数据,不推荐生产
// === 优化 5:使用 mongoose bulkWrite ===
const result = await Product.bulkWrite(
data.map(doc => ({
insertOne: { document: doc }
})),
{ ordered: false }
);
(3) 批量大小选择
| 数据大小 | 推荐批量 | 原因 |
|---|---|---|
| < 100 KB | 1000-5000 | 网络开销低 |
| 100 KB - 1 MB | 500-2000 | 平衡吞吐和延迟 |
| > 1 MB | 100-500 | 避免单次请求过大 |
| 超大文档(接近 16 MB) | 1-10 | 文档本身已大 |
▶ 示例 6:高性能数据导入脚本
// === 导入 100 万产品数据(优化版)===
const fs = require('fs');
const readline = require('readline');
const { MongoClient } = require('mongodb');
async function importLargeDataset() {
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
const collection = client.db('shopdb').collection('products');
// 1. 临时删除索引(导入速度 ↑5x)
await collection.dropIndexes().catch(() => {});
await collection.createIndex({ sku: 1 }, { unique: true }); // 保留唯一索引(防重复)
// 2. 流式读取 CSV
const fileStream = fs.createReadStream('products.csv');
const rl = readline.createInterface({ input: fileStream });
let buffer = [];
const BATCH_SIZE = 2000;
for await (const line of rl) {
const [sku, title, price, category] = line.split(',');
buffer.push({
sku,
title,
price: price ? NumberDecimal(price) : null,
category,
createdAt: new Date()
});
if (buffer.length >= BATCH_SIZE) {
try {
await collection.insertMany(buffer, { ordered: false });
} catch (err) {
if (err.writeErrors) {
console.warn(`⚠️ 跳过 ${err.writeErrors.length} 条重复`);
}
}
buffer = [];
}
}
// 3. 插入剩余数据
if (buffer.length > 0) {
await collection.insertMany(buffer, { ordered: false });
}
// 4. 重建索引
await collection.createIndex({ category: 1, price: 1 });
await collection.createIndex({ title: 'text' });
console.log(`✅ 导入完成`);
await client.close();
}
importLargeDataset().catch(console.error);
输出:
// 执行成功
8. 特殊类型插入
概念说明:MongoDB 的 BSON 格式支持远超 JSON 的数据类型。插入文档时,正确使用这些特殊类型是避免数据精度丢失和类型错误的关键。最常见的三种精度问题是:(1) JavaScript 的 Number 是双精度浮点,0.1 + 0.2 ≠ 0.3;(2) JSON 没有日期类型,new Date() 会被 JSON.stringify() 转为字符串;(3) JSON 不支持二进制数据,图片和文件无法直接存储。
工作原理:MongoDB 驱动程序(包括 mongosh 和 Node.js driver)在发送插入请求前,先将 JavaScript 对象序列化为 BSON。在这个过程中,Date 对象被序列化为 BSON Date 类型(64 位毫秒时间戳),NumberDecimal() 被序列化为 Decimal128(128 位高精度),Buffer 被序列化为 BSON Binary。理解这个序列化过程是正确使用特殊类型的关键。
graph TB
A[JavaScript 对象] --> B[驱动程序序列化]
B --> C{字段类型判断}
C -->|Date 对象| D[BSON Date<br/>64-bit 毫秒时间戳]
C -->|NumberDecimal| E[BSON Decimal128<br/>128-bit 高精度]
C -->|Number 常量| F[BSON Double<br/>64-bit 浮点]
C -->|Buffer / BinData| G[BSON Binary<br/>子类型 + 字节流]
C -->|ObjectId| H[BSON ObjectId<br/>12 字节]
C -->|null| I[BSON Null]
style E fill:#d4edda
style D fill:#d4edda
| 类型 | 语法 | 精度/范围 | 典型场景 |
|---|---|---|---|
Date |
new Date() / ISODate("...") |
毫秒精度 | 时间戳、有效期 |
Decimal128 |
NumberDecimal("0.30") |
34 位十进制 | 金额、精确计算 |
Int32 |
NumberInt(123) |
-2^31 ~ 2^31-1 | 计数、库存 |
Long |
NumberLong(1700000000) |
-2^63 ~ 2^63-1 | 时间戳ID、大整数 |
BinData |
BinData(0, "base64...") |
任意二进制 | 图片、PDF |
ObjectId |
ObjectId() / new ObjectId() |
12字节 | 文档引用、主键 |
(1) 插入日期
// === 当前时间 ===
db.logs.insertOne({ event: "login", timestamp: new Date() });
// === 指定时间 ===
db.logs.insertOne({
event: "signup",
timestamp: ISODate("2026-07-01T10:30:00Z")
});
// === 从字符串创建 ===
db.logs.insertOne({
event: "purchase",
timestamp: new Date("2026-07-01")
});
(2) 插入 ObjectId
// === 自动生成 ===
db.users.insertOne({ name: "Alice" });
// === 手动创建 ===
db.users.insertOne({
_id: new ObjectId(),
name: "Bob"
});
// === 从 24 位 hex 字符串创建 ===
db.users.insertOne({
_id: ObjectId("507f1f77bcf86cd799439011"),
name: "Charlie"
});
// === 从时间戳创建(用于范围查询)===
const startOfDay = ObjectId.createFromTime(
Math.floor(new Date('2026-07-01').getTime() / 1000)
);
db.orders.insertOne({
_id: startOfDay,
total: 999.99
});
(3) 插入嵌套文档
// === 嵌套对象 ===
db.products.insertOne({
sku: "PHONE-001",
specs: {
screen: { size: "6.5", type: "OLED" },
battery: { capacity: "4500mAh", type: "Li-Po" }
}
});
// === 数组 ===
db.products.insertOne({
sku: "SHIRT-001",
sizes: ["S", "M", "L", "XL"],
colors: [
{ name: "Red", hex: "#FF0000" },
{ name: "Blue", hex: "#0000FF" }
]
});
▶ 示例 7:综合类型插入
// === 订单文档(含所有特殊类型)===
db.orders.insertOne({
_id: ObjectId(),
orderNumber: "ORD-20260701-0001",
// 字符串 + 数值
userId: "user_001",
total: NumberDecimal("1299.99"),
tax: NumberDecimal("130.00"),
// 数组 + 嵌套
items: [
{ sku: "LAPTOP-001", qty: 1, price: NumberDecimal("1299.99") },
{ sku: "MOUSE-001", qty: 2, price: NumberDecimal("29.99") }
],
// 状态
status: "pending",
isPaid: false,
// 日期
createdAt: new Date(),
expectedDelivery: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 天后
// 二进制(PDF 收据)
receiptPdf: BinData(0, "JVBERi0xLjQKJ..."),
// 引用
shippingAddressId: ObjectId("507f1f77bcf86cd799439011"),
// 元数据
metadata: {
userAgent: "Mozilla/5.0...",
ipAddress: "192.168.1.1"
}
});
输出:
// 执行成功
9. 常见插入错误排查
概念说明:插入操作可能因多种原因失败——_id 冲突(E11000)、文档验证失败(121)、BSON 文档过大(16755)、字段名非法(2)。理解错误码和处理策略是生产环境稳定运行的保障。错误排查的基本原则是:先看错误码 → 定位错误原因 → 选择处理策略。
工作原理:MongoDB 在写入文档前执行多层校验:BSON 格式校验 → 字段名校验(无 $ 开头、无 .)→ _id 唯一索引校验 → Schema Validation 校验 → 文档大小校验(16MB)→ 嵌套深度校验(100层)。任何一层失败都会阻止写入并返回对应错误码。
调试技巧:
- 启用详细日志:
db.adminCommand({ setParameter: 1, logComponentVerbosity: { write: { verbosity: 2 } } }) - 查看慢查询日志:
db.system.profile.find().sort({ ts: -1 }).limit(5) - 检查文档大小:
BSON.calculateObjectSize(doc)返回字节数 - 检查嵌套深度:自定义
getDepth()函数
生产环境监控:
- 写入延迟:
db.serverStatus().opLatencies.writes监控写入延迟趋势 - 写入吞吐:
db.serverStatus().opcounters.insert监控每秒插入数 - 当前操作:
db.currentOp({ "op": "insert" })查看正在执行的插入操作
graph TB
A[insertOne 请求] --> B{BSON 格式校验}
B -->|失败| B1[错误码 2<br/>字段名非法]
B -->|通过| C{_id 唯一校验}
C -->|冲突| C1[错误码 11000<br/>重复键]
C -->|通过| D{Schema 校验}
D -->|失败| D1[错误码 121<br/>验证失败]
D -->|通过| E{文档大小校验}
E -->|超过16MB| E1[错误码 16755<br/>文档过大]
E -->|通过| F[写入成功 ✅]
style F fill:#d4edda
style C1 fill:#f8d7da
style D1 fill:#f8d7da
| 错误码 | 含义 | 根本原因 | 处理策略 |
|---|---|---|---|
| 11000 | _id 重复 | 已有同 _id 文档 | 使用 upsert 或 ordered: false |
| 121 | 文档验证失败 | 字段值不符合 Schema 规则 | 检查 Schema 验证规则 |
| 2 | 字段名错误 | 字段名以 $ 开头或含 . |
重命名字段 |
| 16755 | BSON 文档过大 | 文档超过 16 MB 限制 | 拆分文档或用 GridFS |
| 14 | Write Concern 超时 | 副本节点响应超时 | 增加 wtimeout 或简化 w |
| 50 | 超过最大 BSON 深度 | 嵌套超过 100 层 | 减少嵌套层级 |
(1) 错误码对照表
| 错误码 | 含义 | 解决方案 |
|---|---|---|
| 11000 | _id 重复 | 使用 upsert 或 ordered: false |
| 121 | 文档验证失败 | 检查 Schema 验证规则 |
| 2 | 字段名错误(如 $ 开头) |
重命名字段 |
| 16755 | BSON 文档过大(>16MB) | 拆分文档或用 GridFS |
| 14 | Write Concern 超时 | 增加 wtimeout 或简化 w |
| 50 | 超过 max BSON 深度 | 减少嵌套层级 |
(2) 调试技巧
// === 启用详细日志 ===
db.adminCommand({ setParameter: 1, logComponentVerbosity: { write: { verbosity: 2 } } });
// === 查看慢查询日志 ===
db.system.profile.find().sort({ ts: -1 }).limit(5);
// === 检查文档大小 ===
const doc = { /* your document */ };
print(`文档大小:${BSON.calculateObjectSize(doc)} bytes`);
print(`嵌套深度:${getDepth(doc)}`);
(3) 性能监控
// === 查看当前数据库操作 ===
db.currentOp({ "op": "insert" });
// === 监控写入性能 ===
db.serverStatus().opcounters;
// {
// insert: 12345,
// query: 67890,
// update: 2345,
// delete: 100,
// ...
// }
// === 查看写入延迟 ===
db.serverStatus().opLatencies.writes;
// { latency: 12345, ops: 10000 }
❓ 常见问题
const { _id, ...rest } = doc),让 MongoDB 自动生成。或者在导入前清空原有 _id 字段。📖 小节
- insertOne 插入单个文档,返回 acknowledged 和 insertedId
- insertMany 批量插入,推荐 ordered: false 跳过失败
- Write Concern 控制写入确认级别:w: 0 / 1 / majority + j: true
- _id 冲突有 5 种处理策略:跳过 / 覆盖 / upsert / 忽略 _id / 重试
- 批量大小推荐 500-5000 条/批,可提升性能 10-100 倍
- 性能优化:bulkWrite + dropIndexes(导入时)+ Write Concern 选择
- 常见错误码 11000(重复)、121(验证失败)、16755(过大)
📝 作业
-
基础题(⭐):用 insertOne 插入 3 个不同类型的产品文档(含 Decimal128、Date、Array、Object),验证返回的 insertedId。
-
基础题(⭐):用 insertMany 一次性插入 10 个用户文档,故意制造 _id 重复,对比 ordered: true 和 ordered: false 的结果差异。
-
进阶题(⭐⭐):编写脚本批量插入 1000 个产品文档(随机生成 sku、title、price),使用 ordered: false 和 Write Concern w: majority,记录插入耗时。
-
进阶题(⭐⭐):使用 bulkWrite 实现"如果 _id 已存在则更新,否则插入"的逻辑(upsert 模式),处理 100 条混合数据。
-
进阶题(⭐⭐):编写性能测试脚本,对比单条插入(1000 次 insertOne)和批量插入(10 次 insertMany 100 条/批)的耗时差异,分析性能差异原因。
-
挑战题(⭐⭐⭐):编写完整的数据迁移工具,从 MySQL 读取 100 万条订单数据(含 Decimal、DateTime),转换为 MongoDB BSON 格式后批量导入,支持:(a) 增量同步;(b) 失败重试;(c) 进度显示;(d) 性能监控。