MongoDB 的灵活性是其最大优势也是最大陷阱——没有固定的 schema 意味着可以自由设计,但也意味着设计不当会导致性能灾难。文档模型的核心决策是何时嵌套、何时引用。
1. 设计原则
1.1 嵌套 vs 引用决策树
数据是否一起查询?
├── 是 → 一起修改?
│ ├── 是 → 嵌套(Embedded)
│ └── 否 → 数据量小且不增长?
│ ├── 是 → 嵌套
│ └── 否 → 引用(Reference)
└── 否 → 引用
1.2 黄金法则
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 主从关系,从数据小且稳定 | 嵌套 | 一次查询获取全部 |
| 主从关系,从数据大或频繁增长 | 引用 | 避免文档膨胀 |
| 多对多关系 | 引用 | 避免重复数据 |
| 需要独立查询从数据 | 引用 | 嵌套后无法单独索引 |
| 原子更新需求 | 嵌套 | 单文档原子操作 |
2. 一对一关系
// 方案 A: 嵌套(推荐,数据总是一起访问)
db.users.findOne({ _id: ObjectId("...") })
// {
// _id: ObjectId("..."),
// name: "Alice",
// profile: {
// bio: "Developer",
// avatar: "https://...",
// preferences: { theme: "dark", lang: "zh" }
// }
// }
// 方案 B: 分离集合(profile 经常独立查询/更新)
db.users.findOne({ _id: uid })
db.user_profiles.findOne({ user_id: uid })
3. 一对多关系
3.1 子文档数组(1:Few)
// 一个用户有少量地址(5个以内)
db.users.insertOne({
_id: ObjectId("..."),
name: "Alice",
addresses: [
{ type: "home", city: "Beijing", zip: "100000", default: true },
{ type: "work", city: "Shanghai", zip: "200000" }
]
});
// 查询默认地址
db.users.find(
{ "addresses.default": true },
{ "addresses.$": 1 } // 位置操作符只返回匹配元素
);
// 新增地址
db.users.updateOne(
{ _id: userId },
{ $push: { addresses: newAddress } }
);
// 更新指定地址
db.users.updateOne(
{ _id: userId, "addresses.type": "home" },
{ $set: { "addresses.$.city": "Shenzhen" } }
);
3.2 父引用(1:Many)
// 一个作者有多篇文章(数量无上限)
// 文章集合存储作者引用
db.articles.insertOne({
_id: ObjectId("..."),
title: "MongoDB Design Patterns",
author_id: ObjectId("author_id_here"), // 引用作者
content: "...",
publishedAt: new Date()
});
// 查询作者的所有文章
db.articles.find({ author_id: authorId }).sort({ publishedAt: -1 });
// 带作者信息的聚合查询
db.articles.aggregate([
{ $match: { author_id: authorId } },
{ $lookup: {
from: "authors",
localField: "author_id",
foreignField: "_id",
as: "author"
}},
{ $unwind: "$author" } // 将数组展开为对象
]);
3.3 子引用(Denormalization)
// 冗余存储常用字段,避免 $lookup
db.articles.insertOne({
title: "MongoDB Design",
author_id: ObjectId("..."),
author_name: "Alice Chen", // 冗余作者名(读多写少场景)
author_avatar: "https://...", // 冗余头像
content: "..."
});
// 作者改名时更新所有文章(写放大)
db.articles.updateMany(
{ author_id: authorId },
{ $set: { author_name: "Alice Wang" } }
);
4. 多对多关系
// 学生和课程的多对多关系
db.students.insertOne({
_id: ObjectId("s1"),
name: "Alice",
enrolled_courses: [ObjectId("c1"), ObjectId("c2")] // 引用课程
});
db.courses.insertOne({
_id: ObjectId("c1"),
name: "MongoDB Basics",
enrolled_students: [ObjectId("s1"), ObjectId("s2")] // 引用学生
});
// 查询某学生的所有课程(双向引用,两查询)
const student = db.students.findOne({ _id: s1 });
db.courses.find({ _id: { $in: student.enrolled_courses } });
// 中间集合方式(更灵活,可存储关系属性)
db.enrollments.insertOne({
student_id: ObjectId("s1"),
course_id: ObjectId("c1"),
enrolledAt: new Date(),
grade: 95,
semester: "2024-spring"
});
5. 设计模式
5.1 桶模式(Bucket Pattern)
用于时序数据,将一段时间的数据放入一个文档:
// 传统方式: 每条读数一个文档 → 集合爆炸式增长
db.readings.insertMany([
{ sensor_id: "s1", timestamp: ISODate("2024-01-01T00:00:00Z"), value: 23.5 },
{ sensor_id: "s1", timestamp: ISODate("2024-01-01T00:01:00Z"), value: 23.6 },
// ... 每分钟一条
]);
// 桶模式: 每小时一个文档
db.sensor_buckets.insertOne({
sensor_id: "s1",
date: ISODate("2024-01-01T00:00:00Z"), // 小时的起点
measurements: [
{ ts: ISODate("2024-01-01T00:00:00Z"), temp: 23.5, humidity: 60 },
{ ts: ISODate("2024-01-01T00:01:00Z"), temp: 23.6, humidity: 61 },
// ... 最多 60 条/小时
],
count: 60,
min_temp: 22.1,
max_temp: 25.8,
avg_temp: 23.7
});
桶模式优势:
- 减少文档数量(1/60)
- 减少索引大小
- 聚合统计更高效(预计算 min/max/avg)
5.2 近似计数模式
// 维护计数器的近似值(避免高频并发更新)
db.products.updateOne(
{ _id: productId },
{ $inc: { view_count: 1, approximate_views: Math.random() > 0.9 ? 1 : 0 } }
// 只有 10% 概率更新 approximate_views,降低写压力
);
5.3 预聚合模式
// 订单集合记录日汇总,避免实时聚合
db.daily_sales.insertOne({
date: ISODate("2024-01-15"),
total_amount: 158000,
order_count: 320,
avg_order_value: 493.75,
category_breakdown: {
electronics: { count: 120, amount: 96000 },
clothing: { count: 200, amount: 62000 }
}
});
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。