MongoDB: 插入文档:insertOne与insertMany详解

最后更新:2026-08-26

文档插入是 MongoDB 数据写入的第一步——掌握 insertOne 与 insertMany 是数据操作的基础。

本课程深入学习插入文档的各种方法、错误处理、性能优化,以及 Write Concern 写入关注机制。

1. 你将学到


2. 一个数据工程师的真实故事

(1) 痛点:批量导入 100 万条数据经常失败

Alice 是一家电商公司的数据工程师,需要将 MySQL 的 100 万条产品数据迁移到 MongoDB:

"我用 insertMany 导入 100 万条产品数据,第 50 万条因为 _id 重复整个导入失败,浪费了 4 小时。要么全成功要么全失败,这对增量迁移简直是灾难。"

她面临的问题:

问题 影响
ordered: true 默认 中间一条失败,整批失败
缺少 Write Concern 服务器宕机导致数据丢失
_id 冲突 MySQL 自增 ID 重复导致失败
大批量插入 100 万次单独插入,耗时 1 小时

(2) MongoDB + bulkWrite 的解法

JAVASCRIPT
// === 使用 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。整个写入过程对单文档是原子的。

100%
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) 基本语法

JAVASCRIPT
// === 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')
// }

要点解析

  1. acknowledged: true 表示写入操作已被 MongoDB 服务端确认(受 Write Concern 影响)
  2. 如果 writeConcern: { w: 0 },则 acknowledgedfalse,且不返回 insertedId
  3. insertedId 的值取决于是否手动指定 _id——未指定则自动生成 ObjectId

(2) 返回值解析

概念说明insertOne 的返回值包含两个关键字段——acknowledgedinsertedIdacknowledgedtrue 表示写入操作已被 MongoDB 服务端确认(受 Write Concern 影响,如果 w: 0 则为 false)。insertedId 是插入文档的 _id 值,无论 _id 是自动生成还是手动指定,都会返回。

返回字段 类型 说明 注意事项
acknowledged Boolean 写入是否已确认 w: 0 时为 false
insertedId ObjectId/任意 插入文档的 _id 手动指定时返回指定值
JAVASCRIPT
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 字节递增计数器组成。时间戳部分使其天然按插入时间排序,随机值保证跨进程唯一性,计数器保证同一秒内唯一。

100%
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("...") 跨系统唯一 ❌ 无序
JAVASCRIPT
// === 不指定 _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 跳过重复
JAVASCRIPT
// === _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 用法

JAVASCRIPT
// === 插入不同类型的字段 ===
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
});

输出:

TEXT 📖 仅展示
// 执行成功

▶ 示例 2:insertOne 带不同 _id 策略

JAVASCRIPT
// === 策略 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 一次插入多条文档,是批量数据写入的核心方法。相比逐条 insertOneinsertMany 将多条文档合并为一次网络请求发送到服务端,大幅减少网络往返开销,性能可提升 10-100 倍。

工作原理insertMany 接收文档数组,按 ordered 选项决定执行策略。ordered: true(默认)按顺序逐条插入,遇到错误立即停止;ordered: false 允许并行插入,跳过失败项继续执行。两种策略对性能和数据完整性影响显著。

100%
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) 基本语法

JAVASCRIPT
// === 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 选项(关键!)

概念说明orderedinsertMany 最关键的选项。它决定了 MongoDB 如何处理批量写入中的错误——是立即中止还是跳过继续。理解 ordered 对生产环境的数据导入至关重要。

使用场景:数据迁移和增量同步场景推荐 ordered: false,因为源数据可能包含重复 _id,跳过重复继续导入比整批失败更合理。金融事务场景推荐 ordered: true,保证操作的严格顺序性。

JAVASCRIPT
// === 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:完整的批量插入实战

JAVASCRIPT
// === 电商产品批量插入示例 ===
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}`));
}

输出:

TEXT 📖 仅展示
// 执行成功

5. Write Concern 写入关注

概念说明:Write Concern 是 MongoDB 的写入安全机制,定义了"一次写入操作何时算成功"。它控制写入操作必须在多少个副本节点确认后才返回客户端。这是数据持久性与写入性能之间的核心权衡——确认级别越高越安全,但延迟也越大。

工作原理:在副本集架构中,写入操作先到达 Primary 节点,再异步复制到 Secondary 节点。Write Concern 的 w 参数决定需要等待多少个节点确认。w: 1 只等 Primary 确认(最快但有丢失风险),w: "majority" 等待大多数节点确认(推荐生产环境),j: true 确保数据已写入磁盘日志(journal)。

100%
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 描述写入操作的成功确认级别,决定数据何时算"已保存"。

100%
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 级别对比

JAVASCRIPT
// === 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 配置

JAVASCRIPT
// === 集群级别设置(推荐)===
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 }
});

输出:

TEXT 📖 仅展示
// mongoose 操作成功执行
// 数据库查询/更新结果

6. _id 冲突处理策略

概念说明_id 是 MongoDB 文档的唯一标识,每个集合的 _id 字段自动创建唯一索引。当插入的文档 _id 与已有文档重复时,MongoDB 抛出 E11000 duplicate key error。在数据迁移、批量导入、多源合并等场景中,_id 冲突是最常见的问题之一。

工作原理:MongoDB 在写入文档前,首先检查 _id 字段是否违反唯一索引约束。如果冲突,整个写入操作回滚(单文档原子性),并返回错误码 11000。理解不同冲突处理策略的区别,对生产环境的数据完整性至关重要。

100%
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) 错误现象

JAVASCRIPT
// === _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 种处理策略

100%
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) 策略实现

JAVASCRIPT
// === 策略 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:批量导入 + 去重策略

JAVASCRIPT
// === 场景:导入 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 }
);

输出:

TEXT 📖 仅展示
// 执行成功

7. 批量插入性能优化

概念说明:批量插入的性能瓶颈主要来自三个方面:网络往返开销、索引更新开销和磁盘 I/O 开销。理解这三个瓶颈并逐一优化,可以将 100 万条数据的导入时间从 60 分钟缩短到 3 分钟以内。

工作原理:逐条 insertOne 每次都触发一次网络请求 + 一次索引更新 + 一次磁盘写入。insertMany 将多条文档合并为一次网络请求,减少 2/3 的开销。bulkWrite 进一步支持混合操作类型(insert+update+delete),在单次请求中完成。导入前临时删除非必要索引,可再提升 5-10 倍性能。

100%
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) 性能对比

100%
graph LR
    A[插入 100 万条数据] --> B[逐条插入<br/>~60 分钟]
    A --> C[insertMany 1000/批<br/>~5 分钟]
    A --> D[bulkWrite 1000/批<br/>~3 分钟]

    style D fill:#d4edda

(2) 优化策略

JAVASCRIPT
// === 优化 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:高性能数据导入脚本

JAVASCRIPT
// === 导入 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);

输出:

TEXT 📖 仅展示
// 执行成功

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。理解这个序列化过程是正确使用特殊类型的关键。

100%
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) 插入日期

JAVASCRIPT
// === 当前时间 ===
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

JAVASCRIPT
// === 自动生成 ===
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) 插入嵌套文档

JAVASCRIPT
// === 嵌套对象 ===
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:综合类型插入

JAVASCRIPT
// === 订单文档(含所有特殊类型)===
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"
  }
});

输出:

TEXT 📖 仅展示
// 执行成功

9. 常见插入错误排查

概念说明:插入操作可能因多种原因失败——_id 冲突(E11000)、文档验证失败(121)、BSON 文档过大(16755)、字段名非法(2)。理解错误码和处理策略是生产环境稳定运行的保障。错误排查的基本原则是:先看错误码 → 定位错误原因 → 选择处理策略。

工作原理:MongoDB 在写入文档前执行多层校验:BSON 格式校验 → 字段名校验(无 $ 开头、无 .)→ _id 唯一索引校验 → Schema Validation 校验 → 文档大小校验(16MB)→ 嵌套深度校验(100层)。任何一层失败都会阻止写入并返回对应错误码。

调试技巧

  1. 启用详细日志:db.adminCommand({ setParameter: 1, logComponentVerbosity: { write: { verbosity: 2 } } })
  2. 查看慢查询日志:db.system.profile.find().sort({ ts: -1 }).limit(5)
  3. 检查文档大小:BSON.calculateObjectSize(doc) 返回字节数
  4. 检查嵌套深度:自定义 getDepth() 函数

生产环境监控

100%
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) 调试技巧

JAVASCRIPT
// === 启用详细日志 ===
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) 性能监控

JAVASCRIPT
// === 查看当前数据库操作 ===
db.currentOp({ "op": "insert" });

// === 监控写入性能 ===
db.serverStatus().opcounters;
// {
//   insert: 12345,
//   query: 67890,
//   update: 2345,
//   delete: 100,
//   ...
// }

// === 查看写入延迟 ===
db.serverStatus().opLatencies.writes;
// { latency: 12345, ops: 10000 }

❓ 常见问题

Q insertOne 和 insertMany 性能差多少?
A insertMany 比多次 insertOne 快 10-100 倍,因为:(1) 减少网络往返;(2) MongoDB 服务端批量处理;(3) 减少索引更新次数。建议批量大小 500-5000 条。
Q _id 必须自己生成吗?
A 不必须。如果不指定,MongoDB 自动生成 ObjectId(时间戳 + 随机值 + 计数器),全球唯一。手动指定适用于需要业务主键(如订单号)的场景。
Q 为什么 ordered: false 性能更好?
A ordered: true 时 MongoDB 按顺序插入,发现错误就停止;ordered: false 时并行插入,发现错误仅跳过失败的,性能更高。生产环境推荐 ordered: false。
Q Write Concern majority 一定会丢失数据吗?
A 在副本集中不会。Primary 写入后,需大多数 Secondary 确认才返回。但如果有节点宕机,可能写入很慢或超时。配置合理的 wtimeout(如 5 秒)可解决。
Q 批量插入多大合适?
A 推荐 500-5000 条/批,或根据数据大小调整(每批 < 16MB)。批量过大会导致单次请求时间过长,批量过小会增加网络开销。
Q 插入时如何跳过 _id 字段?
A 在应用层删除 _id 字段(如 const { _id, ...rest } = doc),让 MongoDB 自动生成。或者在导入前清空原有 _id 字段。
Q 插入性能瓶颈在哪里?
A 常见瓶颈:(1) 唯一索引校验(unique index);(2) Write Concern 等待(w: majority);(3) 副本集同步;(4) 磁盘 IO。优化方法:先 dropIndexes(保留 unique),导入后重建。

📖 小节


📝 作业

  1. 基础题(⭐):用 insertOne 插入 3 个不同类型的产品文档(含 Decimal128、Date、Array、Object),验证返回的 insertedId。

  2. 基础题(⭐):用 insertMany 一次性插入 10 个用户文档,故意制造 _id 重复,对比 ordered: true 和 ordered: false 的结果差异。

  3. 进阶题(⭐⭐):编写脚本批量插入 1000 个产品文档(随机生成 sku、title、price),使用 ordered: false 和 Write Concern w: majority,记录插入耗时。

  4. 进阶题(⭐⭐):使用 bulkWrite 实现"如果 _id 已存在则更新,否则插入"的逻辑(upsert 模式),处理 100 条混合数据。

  5. 进阶题(⭐⭐):编写性能测试脚本,对比单条插入(1000 次 insertOne)和批量插入(10 次 insertMany 100 条/批)的耗时差异,分析性能差异原因。

  6. 挑战题(⭐⭐⭐):编写完整的数据迁移工具,从 MySQL 读取 100 万条订单数据(含 Decimal、DateTime),转换为 MongoDB BSON 格式后批量导入,支持:(a) 增量同步;(b) 失败重试;(c) 进度显示;(d) 性能监控。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏