02. MongoDB 文档模型设计:嵌套 vs 引用

MongoDB 文档模型设计方法论: 一对一、一对多、多对多关系建模,嵌套与引用的权衡

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 }
    }
});

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「mongodb」更多文章

  1. 11. MongoDB 安全认证与备份恢复
  2. 10. MongoDB 性能调优与运维监控
  3. 09. Spring Data MongoDB 实战