MongoDB: 数组与嵌套文档查询

最后更新:2026-08-26

嵌套文档和数组是 MongoDB 的核心特性——掌握它们才能用好文档模型。

本课程深入学习数组查询、嵌套文档点表示法、$elemMatch 精确匹配、数组更新操作。

1. 你将学到


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

(1) 痛点:嵌套文档查询返回错误结果

Alice 维护商品评论系统,遇到了一个查询 bug:

JAVASCRIPT
// ❌ 反例:查询"评分 >= 4 且含 'good' 关键词"的评论
db.products.find({
  "reviews.rating": { $gte: 4 },
  "reviews.content": /good/i
});
// ⚠️ 问题:可能匹配到不同评论(一条评论高评分,另一条含 good)
// 期望:同一条评论同时满足两个条件

(2) $elemMatch 精确匹配的解法

JAVASCRIPT
// ✅ 正例:用 $elemMatch
db.products.find({
  reviews: {
    $elemMatch: {
      rating: { $gte: 4 },
      content: /good/i
    }
  }
});
// ✅ 正确:同一条评论同时满足

100%
graph TB
    subgraph "嵌入式文档"
        A1[products 集合] --> A2[文档 1<br/>address: {<br/>  city: Tokyo<br/>  country: Japan<br/>}]
    end

    subgraph "引用式文档"
        B1[products 集合] --> B2[文档 1<br/>addressId: ObjectId]
        B3[addresses 集合] --> B4[文档 1<br/>city: Tokyo]
        B2 -.->|查询| B3
    end

    style A2 fill:#d4edda

3. 嵌套文档点表示法

概念说明:嵌套文档(Embedded Document)是指一个文档内部包含另一个文档作为字段值。MongoDB 支持最深 100 层嵌套,生产环境建议不超过 3 层。点表示法(Dot Notation)是用 "field.subfield.subsubfield" 语法访问嵌套字段的方式,是 MongoDB 查询和更新嵌套数据的核心手段。

工作原理:MongoDB 解析点表示法时,从外到内逐层定位字段路径。查询引擎将 "specs.screen.size" 拆解为 doc.specs.screen.size,沿 BSON 文档树逐级访问。索引同样支持点表示法字段,如 { "specs.screen.size": 1 } 可创建高效索引。

100%
graph TD
    A[BSON 文档] --> B[specs: Object]
    B --> C[screen: Object]
    C --> D["size: '6.5'"]
    C --> E["type: 'OLED'"]
    B --> F[battery: Object]
    F --> G["capacity: '4500mAh'"]
    
    H["specs.screen.size"] -->|点表示法解析| D
    I["specs.battery.capacity"] -->|点表示法解析| G
    
    style D fill:#d4edda
    style G fill:#d4edda
维度 说明
适用场景 层级固定、一起读写的数据(如地址、规格)
不适用场景 层级不确定、频繁独立更新的数据(用引用)
性能影响 单层≈普通字段;多层需遍历,建议≤3层
索引支持 完全支持,{ "a.b.c": 1 } 等同普通索引

(1) 嵌套文档查询与更新

JAVASCRIPT
// === 嵌套文档结构 ===
db.products.insertOne({
  sku: 'PHONE-001',
  title: 'Smartphone X',
  specs: {
    screen: { size: '6.5', type: 'OLED' },
    battery: { capacity: '4500mAh', type: 'Li-Po' }
  }
});

// === 点表示法查询 ===
db.products.find({ "specs.screen.size": '6.5' });

// === 多层嵌套 ===
db.products.find({ "specs.battery.capacity": '4500mAh' });

// === 嵌套字段更新 ===
db.products.updateOne(
  { sku: 'PHONE-001' },
  { $set: { "specs.battery.capacity": "5000mAh" } }
);

4. 数组字段查询

概念说明:数组是 MongoDB 文档模型中最灵活的数据结构之一。一个字段可以存储多个值,MongoDB 对数组字段的查询遵循"任一匹配"原则——只要数组中有一个元素满足条件,该文档即被选中。这使数组查询既强大又容易产生意外结果。

工作原理:MongoDB 查询引擎对数组字段采用"隐式展开匹配"策略。当查询 { tags: '5g' } 时,引擎逐个检查数组元素,任一元素等于 '5g' 即命中。对于多条件查询(如 {"reviews.rating": {$gte:4}, "reviews.content": /good/}),每个条件独立匹配数组元素,可能命中不同元素——这就是 $elemMatch 存在的原因。

100%
graph LR
    A[查询条件] --> B{数组匹配策略}
    B -->|单条件| C[任一元素匹配<br/>tags: '5g']
    B -->|多条件点表示法| D[各条件独立匹配<br/>可能命中不同元素]
    B -->|多条件 $elemMatch| E[同一元素必须<br/>同时满足所有条件]
    
    C --> F[✅ 简单高效]
    D --> G[⚠️ 可能不准确]
    E --> H[✅ 精确匹配]
    
    style F fill:#d4edda
    style G fill:#fff3cd
    style H fill:#d4edda
查询方式 匹配规则 适用场景
{ tags: '5g' } 任一元素等于 '5g' 单值匹配
{ tags: { $all: ['5g','amoled'] } } 必须同时包含所有值 多值包含
{ tags: { $in: ['5g','fast'] } } 包含任一值 多值或匹配
{ "tags.0": '5g' } 指定索引位置 位置匹配
{ tags: { $size: 3 } } 数组长度精确匹配 长度筛选

(1) 单元素匹配

JAVASCRIPT
// === 数组包含指定元素 ===
db.products.find({ tags: '5g' });
// tags 数组中包含 '5g' 的所有商品

// === 数组包含所有指定元素($all)===
db.products.find({ tags: { $all: ['5g', 'amoled'] } });
// 必须同时包含 '5g' 和 'amoled'

// === 数组包含任一元素($in)===
db.products.find({ tags: { $in: ['5g', 'fast-charging'] } });
// 包含 '5g' 或 'fast-charging'

(2) 数组索引元素查询

JAVASCRIPT
// === 查询数组第一个元素 ===
db.products.find({ "tags.0": '5g' });
// tags[0] = '5g'

// === 查询数组第二个元素 ===
db.products.find({ "tags.1": 'amoled' });
// tags[1] = 'amoled'

// === 数组元素范围查询 ===
db.products.find({ "tags.0": { $in: ['new', 'sale'] } });

(3) 数组长度查询

JAVASCRIPT
// === 数组长度精确匹配 ===
db.products.find({ tags: { $size: 3 } });
// 标签数量 = 3

// === 数组长度范围查询 ===
db.products.find({
  $expr: { $gt: [{ $size: '$tags' }, 3] }
});
// 标签数量 > 3

5. $elemMatch 精确匹配

概念说明$elemMatch 是 MongoDB 专门为数组字段设计的精确匹配操作符。它确保同一个数组元素同时满足所有查询条件,而不是不同元素各自满足部分条件。当数组元素是对象(如评论、成绩)且需要多字段联合查询时,$elemMatch 是唯一正确选择。

工作原理$elemMatch 对数组中每个元素逐一应用所有条件。只有当单个元素同时通过全部条件检查时,该元素才算匹配。与点表示法的关键区别:点表示法的多个条件各自扫描数组,可能命中不同元素;$elemMatch 强制同一元素通过所有测试。

100%
sequenceDiagram
    participant Query as 查询引擎
    participant Doc as 文档<br/>reviews: [{rating:5,content:"bad"},<br/>{rating:3,content:"good"}]
    
    Note over Query,Doc: 点表示法查询: reviews.rating>=4 AND reviews.content=/good/
    Query->>Doc: 条件1: rating>=4 → 命中元素0
    Query->>Doc: 条件2: /good/ → 命中元素1
    Doc-->>Query: ⚠️ 不同元素各满足部分条件 → 返回该文档
    
    Note over Query,Doc: $elemMatch 查询
    Query->>Doc: 元素0: rating>=4 ✅, /good/ ❌ → 不匹配
    Query->>Doc: 元素1: rating>=4 ❌ → 不匹配
    Doc-->>Query: ✅ 无元素同时满足 → 不返回
对比维度 点表示法 $elemMatch
匹配粒度 每个条件独立匹配 同一元素必须全部满足
语法 { "a.b": x, "a.c": y } { a: { $elemMatch: { b: x, c: y } } }
准确性 ⚠️ 可能命中不同元素 ✅ 精确到单个元素
性能 略快(可分别用索引) 略慢(需逐元素检查)
索引支持 多条件可分别走索引 需复合索引

(1) 核心问题

JAVASCRIPT
// === 反例:匹配不同数组元素 ===
db.products.find({
  "reviews.rating": { $gte: 4 },
  "reviews.content": /good/i
});
// 可能匹配:review1.rating=5, review2.content="good"
// 即不同评论分别满足条件

// ✅ 正例:$elemMatch 匹配同一元素
db.products.find({
  reviews: {
    $elemMatch: {
      rating: { $gte: 4 },
      content: /good/i
    }
  }
});
// 必须同一条评论同时满足

(2) 复杂条件

JAVASCRIPT
// === 多条件 elemMatch ===
db.products.find({
  reviews: {
    $elemMatch: {
      rating: { $gte: 4 },
      helpful: { $gt: 10 },
      createdAt: { $gte: new Date('2026-01-01') }
    }
  }
});

// === 嵌套 elemMatch ===
db.students.find({
  courses: {
    $elemMatch: {
      name: 'Math',
      score: { $gte: 90 },
      assignments: {
        $elemMatch: {
          submitted: true,
          grade: { $gte: 80 }
        }
      }
    }
  }
});

6. 数组更新修饰符

概念说明:MongoDB 提供了一组专门操作数组字段的更新修饰符:$push(追加)、$pull(删除)、$addToSet(去重追加)、$pop(首尾删除)。配合 $ 位置占位符和 arrayFilters,可以精确更新数组中的特定元素,而无需替换整个数组。

工作原理:数组更新修饰符在 MongoDB 服务端原子性执行。$push 在数组末尾追加元素;$addToSet 先检查是否已存在再追加;$pull 按条件删除元素;$ 占位符指向查询条件匹配的第一个数组元素的位置,$[] 指向所有元素,$[filter] 配合 arrayFilters 条件更新。

100%
graph TB
    A[数组更新修饰符] --> B[$push<br/>追加元素]
    A --> C[$pull<br/>条件删除]
    A --> D[$addToSet<br/>去重追加]
    A --> E[$pop<br/>首尾删除]
    
    F[位置操作符] --> G[$<br/>更新第一个匹配]
    F --> H[$[]<br/>更新所有元素]
    F --> I[$[filter]<br/>条件批量更新<br/>配合arrayFilters]
    
    style B fill:#d4edda
    style C fill:#f8d7da
    style D fill:#cce5ff
    style I fill:#fff3cd
修饰符 功能 示例
$push 追加元素(可重复) { $push: { tags: 'new' } }
$each 配合$push追加多个 { $push: { tags: { $each: ['a','b'] } } }
$addToSet 去重追加 { $addToSet: { tags: 'new' } }
$pull 按条件删除 { $pull: { tags: 'old' } }
$pop 删除首/尾 { $pop: { tags: 1 } } (删末尾)
$ 更新第一个匹配元素 { $set: { "reviews.$.flagged": true } }
$[] 更新所有元素 { $set: { "reviews.$[].status": "ok" } }
$[f] 条件批量更新 { arrayFilters: [{ "f.rating": {$lt:2} }] }

(1) $push 添加元素

JAVASCRIPT
// === 添加单个 ===
db.products.updateOne(
  { sku: 'PHONE-001' },
  { $push: { tags: 'bestseller' } }
);

// === 添加多个($each)===
db.products.updateOne(
  { sku: 'PHONE-001' },
  { $push: { tags: { $each: ['5g', 'amoled', 'fast-charging'] } } }
);

(2) $pull 删除元素

JAVASCRIPT
// === 删除指定值 ===
db.products.updateOne(
  { sku: 'PHONE-001' },
  { $pull: { tags: 'old-tag' } }
);

// === 删除多个 ===
db.products.updateOne(
  { sku: 'PHONE-001' },
  { $pull: { tags: { $in: ['outdated1', 'outdated2'] } } }
);

// === 删除满足条件的元素 ===
db.products.updateOne(
  { sku: 'PHONE-001' },
  { $pull: { reviews: { rating: { $lt: 2 } } } }
);
// 删除评分 < 2 的评论

(3) $ 位置占位符

JAVASCRIPT
// === 更新第一个匹配的数组元素 ===
db.products.updateOne(
  { sku: 'PHONE-001', "reviews.userId": 'user_001' },
  { $set: { "reviews.$.helpful": 10 } }
);
// 更新 user_001 的评论

// === 更新所有匹配的数组元素 ===
db.products.updateOne(
  { sku: 'PHONE-001' },
  { $set: { "reviews.$[].status": "approved" } }
);

// === 条件更新(arrayFilters)===
db.products.updateOne(
  { sku: 'PHONE-001' },
  { $set: { "reviews.$[lowRating].flagged": true } },
  { arrayFilters: [{ "lowRating.rating": { $lt: 2 } }] }
);

7. multikey index 数组索引

概念说明:当索引字段是数组时,MongoDB 自动创建 multikey index(多键索引)。其核心特点是:数组中的每个元素都会生成一个独立的索引键。例如 tags: ['5g', 'amoled'] 会在索引中创建两条记录,分别指向同一文档。这使得数组字段查询性能等同于普通字段索引。

工作原理:MongoDB 在插入文档时检测索引字段是否为数组。如果是,为每个元素创建索引项。查询时,MongoDB 利用多键索引快速定位包含目标元素的文档。限制:一个复合索引最多只能包含一个数组字段,否则会导致索引条目数爆炸(笛卡尔积)。

100%
graph LR
    A[文档<br/>tags: 5g, amoled, fast] --> B[multikey index]
    B --> C["索引项: '5g' → docId"]
    B --> D["索引项: 'amoled' → docId"]
    B --> E["索引项: 'fast' → docId"]
    
    F["查询 {tags: '5g'}"] -->|索引查找| C
    C --> G[✅ 命中文档]
    
    style B fill:#d4edda
    style G fill:#d4edda
multikey 限制 说明
一个数组元素一个索引键 数组长度影响索引大小
复合索引最多 1 个数组字段 防止索引爆炸
数组索引查询性能高 等同普通字段索引
无法对整个数组值建索引 仅索引元素,不索引数组本身

▶ 示例 1:嵌套文档查询实战

JAVASCRIPT
// 场景:ShopHub 电商平台的商品规格查询
db.products.insertOne({
  sku: 'PHONE-002',
  title: 'Smartphone Y',
  specs: {
    screen: { size: '6.7', type: 'AMOLED', refreshRate: '120Hz' },
    battery: { capacity: '5000mAh', type: 'Li-Po', fastCharging: true },
    storage: { ram: '12GB', internal: '256GB' }
  }
});

// 查询:AMOLED 屏幕的商品
db.products.find({ "specs.screen.type": 'AMOLED' });

// 查询:支持快充且电池 ≥ 5000mAh 的商品
db.products.find({
  "specs.battery.fastCharging": true,
  "specs.battery.capacity": { $in: ['5000mAh', '6000mAh'] }
});

// 更新:增加屏幕保护膜信息
db.products.updateOne(
  { sku: 'PHONE-002' },
  { $set: { "specs.screen.protection": "Gorilla Glass 5" } }
);

输出:点表示法精确定位多层嵌套字段,查询和更新都只影响目标字段,不会覆盖整个嵌套对象。

▶ 示例 2:$elemMatch 与数组更新综合实战

JAVASCRIPT
// 场景:ShopHub 商品评论管理
db.products.insertOne({
  sku: 'LAPTOP-001',
  title: 'Laptop Pro',
  reviews: [
    { userId: 'user_010', rating: 2, content: 'Poor quality', helpful: 0, createdAt: new Date('2026-05-01') },
    { userId: 'user_011', rating: 5, content: 'Excellent good value', helpful: 30, createdAt: new Date('2026-06-01') },
    { userId: 'user_012', rating: 4, content: 'Good performance', helpful: 12, createdAt: new Date('2026-07-01') }
  ]
});

// 1. 精确查询:同一评论 rating>=4 且含 'good'
db.products.find({
  reviews: { $elemMatch: { rating: { $gte: 4 }, content: /good/i } }
});
// 只命中 user_011 的评论(rating=5 且含 "good")

// 2. 用 $ 更新第一条高评分评论
db.products.updateOne(
  { sku: 'LAPTOP-001', "reviews.rating": { $gte: 5 } },
  { $set: { "reviews.$.featured": true } }
);

// 3. 用 arrayFilters 标记所有低分评论
db.products.updateOne(
  { sku: 'LAPTOP-001' },
  { $set: { "reviews.$[low].flagged": true } },
  { arrayFilters: [{ "low.rating": { $lt: 3 } }] }
);

// 4. 删除所有低分评论
db.products.updateOne(
  { sku: 'LAPTOP-001' },
  { $pull: { reviews: { rating: { $lt: 3 } } } }
);

输出:$elemMatch 精确匹配同一评论;$ 更新第一个匹配;arrayFilters 批量条件更新;$pull 删除满足条件的评论。

JAVASCRIPT
// === 创建数组字段索引 ===
db.products.createIndex({ tags: 1 });
// 自动成为 multikey index

// === 复合 multikey index ===
db.products.createIndex({ category: 1, tags: 1 });

// === 查看索引 ===
db.products.getIndexes();

▶ 示例 3:数组与嵌套文档综合实战

JAVASCRIPT
// 完整场景:电商商品评论嵌套查询
// 1. 准备数据:包含嵌套评论的产品
db.products.insertOne({
  sku: 'PHONE-001',
  title: 'Smartphone X',
  specs: {
    screen: { size: '6.5', type: 'OLED' },
    battery: { capacity: '4500mAh', type: 'Li-Po' }
  },
  reviews: [
    { userId: 'user_001', rating: 5, content: 'Excellent phone! Great battery', helpful: 15, createdAt: new Date('2026-06-01') },
    { userId: 'user_002', rating: 3, content: 'Good but expensive', helpful: 5, createdAt: new Date('2026-06-15') },
    { userId: 'user_003', rating: 5, content: 'Best phone ever, amazing good quality', helpful: 25, createdAt: new Date('2026-07-01') }
  ]
});

// 2. 查询:评分 >= 4 且含 'good' 关键词的评论(同一条评论满足)
db.products.find({
  reviews: {
    $elemMatch: {
      rating: { $gte: 4 },
      content: /good/i
    }
  }
}, { sku: 1, title: 1, reviews: 1 });

// 3. 更新:给第一条高评分评论加精
db.products.updateOne(
  { sku: 'PHONE-001', 'reviews.rating': { $gte: 5 } },
  { $set: { 'reviews.$.featured': true } }
);

// 4. 数组元素查询:tags 包含 '5g' 且数组长度 = 3
db.products.find({ tags: { $all: ['5g', 'amoled'], $size: 3 } });

// 5. 多层嵌套查询:屏幕尺寸 = 6.5
db.products.find({ 'specs.screen.size': '6.5' });

输出:返回 PHONE-001 商品,reviews 数组中只有第 3 条评论同时满足 rating>=4 和含 'good',正好验证 $elemMatch 的精确匹配能力。

❓ 常见问题

Q $elemMatch 必须用吗?
A 当同一数组需要同时满足多个条件时必须用。如果只有单条件查询(如 { tags: '5g' }),不需要 $elemMatch。
Q 点表示法和 $elemMatch 的区别?
A 点表示法 { "a.b": 1, "a.c": 2 } 可能匹配同一文档但不同数组元素;$elemMatch 必须同一数组元素。
Q 数组字段能建索引吗?
A 能。MongoDB 自动为数组字段创建 multikey index,但一个复合索引最多包含 1 个数组字段。
Q 嵌套文档查询性能好吗?
A 单层嵌套性能接近普通字段。多层嵌套需要点表示法,索引仍生效。建议嵌套不超过 3 层。

📖 小节


📝 作业

  1. 基础题(⭐):查询 tags 包含 "5g" 或 "amoled" 的商品。
  2. 基础题(⭐):用 $pull 删除所有评分 < 2 的评论。
  3. 进阶题(⭐⭐):用 $elemMatch 查询评论中"评分 >= 4 且含 'good' 关键词"的商品。
  4. 进阶题(⭐⭐):用 $ 位置占位符更新指定用户的评论 helpful 字段。
  5. 挑战题(⭐⭐⭐):实现评论系统的点赞功能(helpful +1)、筛选低分评论、批量标记已审核。
Web-Tutorial.com

Web-Tutorial 技术团队

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

100%

🙏 帮我们做得更好

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

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