MongoDB: 数组与嵌套文档查询
最后更新:2026-08-26
嵌套文档和数组是 MongoDB 的核心特性——掌握它们才能用好文档模型。
本课程深入学习数组查询、嵌套文档点表示法、$elemMatch 精确匹配、数组更新操作。
1. 你将学到
- 嵌套文档的点表示法查询
- 数组字段查询(单元素、多元素)
- $elemMatch 精确匹配同一数组元素
$位置占位符更新- 数组索引(multikey index)
- 数组 vs 引用的设计取舍
2. 一个电商数据工程师的真实故事
(1) 痛点:嵌套文档查询返回错误结果
Alice 维护商品评论系统,遇到了一个查询 bug:
// ❌ 反例:查询"评分 >= 4 且含 'good' 关键词"的评论
db.products.find({
"reviews.rating": { $gte: 4 },
"reviews.content": /good/i
});
// ⚠️ 问题:可能匹配到不同评论(一条评论高评分,另一条含 good)
// 期望:同一条评论同时满足两个条件
(2) $elemMatch 精确匹配的解法
// ✅ 正例:用 $elemMatch
db.products.find({
reviews: {
$elemMatch: {
rating: { $gte: 4 },
content: /good/i
}
}
});
// ✅ 正确:同一条评论同时满足
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 } 可创建高效索引。
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) 嵌套文档查询与更新
// === 嵌套文档结构 ===
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 存在的原因。
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) 单元素匹配
// === 数组包含指定元素 ===
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) 数组索引元素查询
// === 查询数组第一个元素 ===
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) 数组长度查询
// === 数组长度精确匹配 ===
db.products.find({ tags: { $size: 3 } });
// 标签数量 = 3
// === 数组长度范围查询 ===
db.products.find({
$expr: { $gt: [{ $size: '$tags' }, 3] }
});
// 标签数量 > 3
5. $elemMatch 精确匹配
概念说明:$elemMatch 是 MongoDB 专门为数组字段设计的精确匹配操作符。它确保同一个数组元素同时满足所有查询条件,而不是不同元素各自满足部分条件。当数组元素是对象(如评论、成绩)且需要多字段联合查询时,$elemMatch 是唯一正确选择。
工作原理:$elemMatch 对数组中每个元素逐一应用所有条件。只有当单个元素同时通过全部条件检查时,该元素才算匹配。与点表示法的关键区别:点表示法的多个条件各自扫描数组,可能命中不同元素;$elemMatch 强制同一元素通过所有测试。
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) 核心问题
// === 反例:匹配不同数组元素 ===
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) 复杂条件
// === 多条件 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 条件更新。
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 添加元素
// === 添加单个 ===
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 删除元素
// === 删除指定值 ===
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) $ 位置占位符
// === 更新第一个匹配的数组元素 ===
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 利用多键索引快速定位包含目标元素的文档。限制:一个复合索引最多只能包含一个数组字段,否则会导致索引条目数爆炸(笛卡尔积)。
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:嵌套文档查询实战
// 场景: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 与数组更新综合实战
// 场景: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删除满足条件的评论。
// === 创建数组字段索引 ===
db.products.createIndex({ tags: 1 });
// 自动成为 multikey index
// === 复合 multikey index ===
db.products.createIndex({ category: 1, tags: 1 });
// === 查看索引 ===
db.products.getIndexes();
▶ 示例 3:数组与嵌套文档综合实战
// 完整场景:电商商品评论嵌套查询
// 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 的精确匹配能力。
❓ 常见问题
{ tags: '5g' }),不需要 $elemMatch。{ "a.b": 1, "a.c": 2 } 可能匹配同一文档但不同数组元素;$elemMatch 必须同一数组元素。📖 小节
- 点表示法查询嵌套文档:
field.subfield - 数组单元素匹配:
{ tags: '5g' } - $all 匹配所有:
{ tags: { $all: ['a', 'b'] } } - $elemMatch 精确匹配同一数组元素
- 数组更新:
$push/$pull/$pop/$addToSet $占位符更新第一个匹配元素
📝 作业
- 基础题(⭐):查询 tags 包含 "5g" 或 "amoled" 的商品。
- 基础题(⭐):用 $pull 删除所有评分 < 2 的评论。
- 进阶题(⭐⭐):用 $elemMatch 查询评论中"评分 >= 4 且含 'good' 关键词"的商品。
- 进阶题(⭐⭐):用 $ 位置占位符更新指定用户的评论 helpful 字段。
- 挑战题(⭐⭐⭐):实现评论系统的点赞功能(helpful +1)、筛选低分评论、批量标记已审核。