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 }
}
});
6. Schema 验证与变更治理
MongoDB 从 3.6 起支持 JSON Schema 验证,在灵活性与结构约束之间取得平衡:
db.createCollection("users", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["email", "createdAt"],
properties: {
email: {
bsonType: "string",
pattern: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
},
role: {
enum: ["user", "admin", "editor"]
},
age: {
bsonType: "int",
minimum: 0,
maximum: 150
}
}
}
},
validationLevel: "moderate",
validationAction: "error"
});
验证级别选择策略:
| 级别 | 行为 | 适用场景 |
|---|---|---|
strict | 所有操作都必须通过验证 | 新集合或可以全量迁移的项目 |
moderate | 仅对已存在有效文档的更新做验证 | 历史存量数据质量参差不齐 |
off | 关闭验证 | 临时数据导入阶段 |
Schema 变更的最佳实践:字段采用版本化命名(v2_address),配合应用层多版本兼容读取,避免停机迁移。
7. 多文档事务与 ACID
MongoDB 4.0+ 支持副本集事务,4.2+ 支持分片集群事务,但仍有重要限制:
const session = db.getMongo().startSession();
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" }
});
try {
const orders = session.getDatabase("shop").orders;
const inventory = session.getDatabase("shop").inventory;
orders.insertOne({ item: "laptop", qty: 1, status: "paid" });
inventory.updateOne(
{ sku: "laptop" },
{ $inc: { qty: -1 } }
);
session.commitTransaction();
} catch (err) {
session.abortTransaction();
throw err;
} finally {
session.endSession();
}
事务使用原则:
- 事务有 60 秒默认超时,事务内文档修改不超过 1000 条
- 优先用单文档原子操作(如
$inc、$set)替代多文档事务 - 事务会显著影响性能(约 30–50% 延迟增加),仅在真正需要时使用
8. 索引策略与查询优化
文档模型设计必须与索引策略协同考虑:
// 复合索引:支持前缀匹配查询
db.orders.createIndex({ status: 1, createdAt: -1 });
// 多键索引(数组字段自动创建)
db.users.createIndex({ "addresses.city": 1 });
// 文本索引(全文搜索前置方案)
db.articles.createIndex({ title: "text", content: "text" });
db.articles.find({ $text: { $search: "mongodb indexing" } });
// 部分索引(仅索引符合条件的文档,节省空间)
db.sessions.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 3600, partialFilterExpression: { active: true } }
);
索引设计检查清单:
- 用
db.collection.explain("executionStats")验证查询是否命中索引 - ESR 规则:等值过滤(Equality)→ 排序(Sort)→ 范围(Range)
- 避免过多索引(写放大),单个集合建议不超过 10–15 个
9. 工作集与内存管理
MongoDB 依赖 WiredTiger 缓存,工作集(频繁访问的数据子集)大小直接影响性能:
// 查看工作集估算(mongosh)
db.serverStatus().wiredTiger.cache
// 比值应 < 80%,超过则需扩容 RAM 或分片
// 查看集合工作集热点
db.collection.stats({ indexDetails: true })
工作集优化策略:
- 冷热分离:热数据放在 SSD,历史归档数据定期迁移到廉价存储
- 分片(Sharding):按时间或地域分片,缩小单个节点工作集
- TTL 索引:自动清理过期数据,防止工作集膨胀
// 30 天后自动删除(适合日志、临时会话)
db.logs.createIndex({ createdAt: 1 }, { expireAfterSeconds: 2592000 });
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。