引言

MongoDB 作为一款流行的 NoSQL 数据库,以其灵活的文档模型和强大的扩展能力而闻名。然而,这种灵活性也带来了设计上的挑战。一个设计良好的数据模型不仅能提升应用性能,还能简化查询逻辑,降低维护成本。本文将深入探讨 MongoDB 数据模型设计的各个方面,从基础的文档结构设计到高级的查询优化策略,帮助你构建高效、可扩展的 MongoDB 应用。

一、理解 MongoDB 的核心:文档模型

1.1 文档结构基础

MongoDB 存储数据的基本单位是文档(Document),它采用 BSON(Binary JSON)格式。一个文档由键值对组成,支持多种数据类型,包括字符串、数字、日期、数组、嵌套文档等。

示例:一个简单的用户文档

{
  "_id": ObjectId("507f1f77bcf86cd799439011"),
  "username": "johndoe",
  "email": "john@example.com",
  "age": 30,
  "is_active": true,
  "created_at": ISODate("2023-01-15T10:00:00Z"),
  "tags": ["developer", "mongodb", "nosql"],
  "address": {
    "street": "123 Main St",
    "city": "New York",
    "zip": "10001"
  }
}

1.2 设计原则:嵌入 vs 引用

在 MongoDB 中,设计数据模型时面临两个主要选择:嵌入(Embedding)引用(Referencing)

嵌入(Embedding)

将相关数据直接嵌入到父文档中。适用于数据之间有紧密的“包含”关系,且查询通常需要同时获取这些数据。

优点

  • 单次查询即可获取所有相关数据
  • 避免了额外的连接操作
  • 数据局部性好,性能高

缺点

  • 文档大小受限(16MB)
  • 数据重复(如果被多个父文档引用)
  • 更新操作可能更复杂

示例:博客文章与评论

// 文章文档(嵌入评论)
{
  "_id": ObjectId("60a1b2c3d4e5f6a7b8c9d0e1"),
  "title": "MongoDB 设计指南",
  "author": "张三",
  "content": "这是一篇关于 MongoDB 的文章...",
  "created_at": ISODate("2023-05-10T08:00:00Z"),
  "comments": [
    {
      "comment_id": ObjectId("60a1b2c3d4e5f6a7b8c9d0e2"),
      "user": "李四",
      "content": "写得真好!",
      "timestamp": ISODate("2023-05-10T09:00:00Z")
    },
    {
      "comment_id": ObjectId("60a1b2c3d4e5f6a7b8c9d0e3"),
      "user": "王五",
      "content": "很有帮助,谢谢!",
      "timestamp": ISODate("2023-05-10T10:00:00Z")
    }
  ]
}

引用(Referencing)

使用引用(通常是 ObjectId)将相关数据存储在不同的集合中。适用于数据之间有松散的“关联”关系,或者数据被多个父文档共享。

优点

  • 避免数据重复
  • 文档大小可控
  • 更新操作更简单

缺点

  • 需要多次查询或使用聚合管道进行连接
  • 可能增加查询复杂度

示例:博客文章与评论(引用)

// 文章集合
{
  "_id": ObjectId("60a1b2c3d4e5f6a7b8c9d0e1"),
  "title": "MongoDB 设计指南",
  "author": "张三",
  "content": "这是一篇关于 MongoDB 的文章...",
  "created_at": ISODate("2023-05-10T08:00:00Z")
}

// 评论集合
{
  "_id": ObjectId("60a1b2c3d4e5f6a7b8c9d0e2"),
  "article_id": ObjectId("60a1b2c3d4e5f6a7b8c9d0e1"),
  "user": "李四",
  "content": "写得真好!",
  "timestamp": ISODate("2023-05-10T09:00:00Z")
}

1.3 选择嵌入还是引用的决策树

开始
  ↓
数据是否紧密相关且总是同时查询? → 是 → 嵌入
  ↓ 否
数据是否被多个父文档共享? → 是 → 引用
  ↓ 否
数据量是否很大(可能超过16MB)? → 是 → 引用
  ↓ 否
数据是否经常独立更新? → 是 → 引用
  ↓ 否
→ 嵌入

二、常见场景的数据模型设计

2.1 电商系统设计

2.1.1 用户与订单

设计选项1:嵌入订单到用户文档

// 用户文档(嵌入订单)
{
  "_id": ObjectId("507f1f77bcf86cd799439011"),
  "username": "alice",
  "email": "alice@example.com",
  "orders": [
    {
      "order_id": ObjectId("60a1b2c3d4e5f6a7b8c9d0e1"),
      "order_number": "ORD-2023-001",
      "total": 150.00,
      "status": "completed",
      "items": [
        {
          "product_id": ObjectId("60a1b2c3d4e5f6a7b8c9d0e2"),
          "name": "Laptop",
          "quantity": 1,
          "price": 150.00
        }
      ],
      "created_at": ISODate("2023-05-10T08:00:00Z")
    }
  ]
}

设计选项2:独立的订单集合

// 用户集合
{
  "_id": ObjectId("507f1f77bcf86cd799439011"),
  "username": "alice",
  "email": "alice@example.com"
}

// 订单集合
{
  "_id": ObjectId("60a1b2c3d4e5f6a7b8c9d0e1"),
  "user_id": ObjectId("507f1f77bcf86cd799439011"),
  "order_number": "ORD-2023-001",
  "total": 150.00,
  "status": "completed",
  "items": [
    {
      "product_id": ObjectId("60a1b2c3d4e5f6a7b8c9d0e2"),
      "name": "Laptop",
      "quantity": 1,
      "price": 150.00
    }
  ],
  "created_at": ISODate("2023-05-10T08:00:00Z")
}

推荐方案

  • 如果用户订单数量有限(如 < 100),且经常需要同时查看用户信息和订单,可以考虑嵌入
  • 如果订单数量可能很大,或者需要独立的订单分析,建议使用独立的订单集合

2.1.2 产品与库存

// 产品集合
{
  "_id": ObjectId("60a1b2c3d4e5f6a7b8c9d0e2"),
  "sku": "LP-001",
  "name": "Laptop Pro",
  "description": "高性能笔记本电脑",
  "price": 1500.00,
  "category": "electronics",
  "attributes": {
    "brand": "TechBrand",
    "model": "Pro X1",
    "specs": {
      "cpu": "Intel i7",
      "ram": "16GB",
      "storage": "512GB SSD"
    }
  },
  "created_at": ISODate("2023-01-01T00:00:00Z")
}

// 库存集合(与产品分离,因为库存变化频繁)
{
  "_id": ObjectId("60a1b2c3d4e5f6a7b8c9d0e3"),
  "product_id": ObjectId("60a1b2c3d4e5f6a7b8c9d0e2"),
  "warehouse_id": ObjectId("60a1b2c3d4e5f6a7b8c9d0e4"),
  "quantity": 50,
  "reserved": 5,
  "last_updated": ISODate("2023-05-10T12:00:00Z")
}

2.2 社交网络设计

2.2.1 用户资料与关注关系

// 用户集合
{
  "_id": ObjectId("507f1f77bcf86cd799439011"),
  "username": "alice",
  "email": "alice@example.com",
  "profile": {
    "display_name": "Alice Chen",
    "bio": "Software Engineer",
    "avatar": "https://example.com/avatar.jpg",
    "location": "San Francisco"
  },
  "stats": {
    "followers": 1250,
    "following": 300,
    "posts": 45
  },
  "created_at": ISODate("2023-01-01T00:00:00Z")
}

// 关注关系集合(使用引用)
{
  "_id": ObjectId("60a1b2c3d4e5f6a7b8c9d0e1"),
  "follower_id": ObjectId("507f1f77bcf86cd799439011"),  // Alice
  "followed_id": ObjectId("507f1f77bcf86cd799439012"),  // Bob
  "created_at": ISODate("2023-05-10T08:00:00Z")
}

2.2.2 帖子与互动

// 帖子集合
{
  "_id": ObjectId("60a1b2c3d4e5f6a7b8c9d0e1"),
  "author_id": ObjectId("507f1f77bcf86cd799439011"),
  "content": "今天天气真好!",
  "media": [
    {
      "type": "image",
      "url": "https://example.com/photo.jpg",
      "caption": "阳光"
    }
  ],
  "stats": {
    "likes": 42,
    "comments": 5,
    "shares": 3
  },
  "created_at": ISODate("2023-05-10T09:00:00Z"),
  "updated_at": ISODate("2023-05-10T09:00:00Z")
}

// 互动集合(点赞、评论等)
{
  "_id": ObjectId("60a1b2c3d4e5f6a7b8c9d0e2"),
  "post_id": ObjectId("60a1b2c3d4e5f6a7b8c9d0e1"),
  "user_id": ObjectId("507f1f77bcf86cd799439012"),
  "type": "like",
  "created_at": ISODate("2023-05-10T09:05:00Z")
}

// 评论集合
{
  "_id": ObjectId("60a1b2c3d4e5f6a7b8c9d0e3"),
  "post_id": ObjectId("60a1b2c3d4e5f6a7b8c9d0e1"),
  "parent_comment_id": null,  // 用于回复
  "user_id": ObjectId("507f1f77bcf86cd799439012"),
  "content": "我也觉得!",
  "created_at": ISODate("2023-05-10T09:10:00Z")
}

2.3 内容管理系统设计

2.3.1 文章与分类

// 文章集合
{
  "_id": ObjectId("60a1b2c3d4e5f6a7b8c9d0e1"),
  "title": "MongoDB 数据模型设计",
  "slug": "mongodb-data-model-design",
  "content": "这是一篇详细的文章...",
  "author_id": ObjectId("507f1f77bcf86cd799439011"),
  "category_ids": [
    ObjectId("60a1b2c3d4e5f6a7b8c9d0e2"),
    ObjectId("60a1b2c3d4e5f6a7b8c9d0e3")
  ],
  "tags": ["mongodb", "database", "design"],
  "status": "published",
  "published_at": ISODate("2023-05-10T08:00:00Z"),
  "created_at": ISODate("2023-05-09T10:00:00Z"),
  "updated_at": ISODate("2023-05-10T07:00:00Z")
}

// 分类集合
{
  "_id": ObjectId("60a1b2c3d4e5f6a7b8c9d0e2"),
  "name": "数据库",
  "slug": "database",
  "description": "关于数据库技术的文章",
  "parent_id": null,
  "created_at": ISODate("2023-01-01T00:00:00Z")
}

2.3.2 页面与组件

// 页面集合(使用嵌套组件)
{
  "_id": ObjectId("60a1b2c3d4e5f6a7b8c9d0e1"),
  "title": "首页",
  "slug": "home",
  "components": [
    {
      "type": "hero",
      "data": {
        "title": "欢迎来到我们的网站",
        "subtitle": "探索更多内容",
        "image": "https://example.com/hero.jpg"
      }
    },
    {
      "type": "featured_posts",
      "data": {
        "count": 3,
        "category": "featured"
      }
    },
    {
      "type": "newsletter",
      "data": {
        "title": "订阅我们的新闻",
        "description": "获取最新资讯"
      }
    }
  ],
  "created_at": ISODate("2023-05-10T08:00:00Z"),
  "updated_at": ISODate("2023-05-10T08:00:00Z")
}

三、索引设计与查询优化

3.1 索引基础

索引是提高查询性能的关键。MongoDB 支持多种索引类型:

  • 单字段索引:对单个字段创建索引
  • 复合索引:对多个字段创建索引
  • 多键索引:对数组字段创建索引
  • 文本索引:支持全文搜索
  • 地理空间索引:支持地理位置查询
  • TTL 索引:自动过期数据
  • 唯一索引:确保字段值唯一

3.2 索引设计原则

3.2.1 索引选择性

选择性高的字段(值分布广)更适合索引。例如,email 字段通常比 gender 字段选择性更高。

// 创建单字段索引
db.users.createIndex({ email: 1 });  // 升序索引
db.users.createIndex({ email: -1 }); // 降序索引

// 查看索引
db.users.getIndexes();

// 删除索引
db.users.dropIndex("email_1");

3.2.2 复合索引顺序

复合索引的字段顺序非常重要。MongoDB 遵循最左前缀原则

// 创建复合索引
db.users.createIndex({ last_name: 1, first_name: 1 });

// 以下查询可以使用该索引
db.users.find({ last_name: "Smith" });
db.users.find({ last_name: "Smith", first_name: "John" });

// 以下查询不能使用该索引(缺少最左字段)
db.users.find({ first_name: "John" });

最佳实践

  1. 将最常查询的字段放在前面
  2. 将等值查询字段放在范围查询字段之前
  3. 考虑排序字段的方向
// 好的复合索引设计
db.orders.createIndex({ user_id: 1, status: 1, created_at: -1 });

// 以下查询可以使用该索引
db.orders.find({ user_id: ObjectId("..."), status: "completed" }).sort({ created_at: -1 });

3.2.3 覆盖查询

覆盖查询是指查询只需要从索引中获取数据,而不需要回表(访问原始文档)。这可以显著提高性能。

// 创建覆盖索引
db.users.createIndex({ email: 1, username: 1, name: 1 });

// 覆盖查询(只返回索引字段)
db.users.find(
  { email: "john@example.com" },
  { _id: 0, email: 1, username: 1, name: 1 }
);

// 检查是否覆盖
db.users.find({ email: "john@example.com" }).explain("executionStats");

3.3 查询优化技巧

3.3.1 使用投影减少数据传输

// 不好的做法:返回所有字段
db.users.find({ age: { $gte: 18 } });

// 好的做法:只返回需要的字段
db.users.find(
  { age: { $gte: 18 } },
  { _id: 1, username: 1, email: 1, age: 1 }
);

3.3.2 使用聚合管道进行复杂查询

// 示例:统计每个用户的订单数量和总金额
db.orders.aggregate([
  {
    $match: { status: "completed" }
  },
  {
    $group: {
      _id: "$user_id",
      total_orders: { $sum: 1 },
      total_amount: { $sum: "$total" }
    }
  },
  {
    $lookup: {
      from: "users",
      localField: "_id",
      foreignField: "_id",
      as: "user_info"
    }
  },
  {
    $unwind: "$user_info"
  },
  {
    $project: {
      username: "$user_info.username",
      email: "$user_info.email",
      total_orders: 1,
      total_amount: 1
    }
  },
  {
    $sort: { total_amount: -1 }
  },
  {
    $limit: 10
  }
]);

3.3.3 使用 $lookup 进行关联查询

// 示例:获取文章及其作者信息
db.articles.aggregate([
  {
    $match: { status: "published" }
  },
  {
    $lookup: {
      from: "users",
      localField: "author_id",
      foreignField: "_id",
      as: "author"
    }
  },
  {
    $unwind: "$author"
  },
  {
    $project: {
      title: 1,
      content: 1,
      "author.username": 1,
      "author.email": 1,
      created_at: 1
    }
  }
]);

3.4 性能监控与调优

3.4.1 使用 explain() 分析查询

// 基本使用
db.users.find({ email: "john@example.com" }).explain("executionStats");

// 输出示例
{
  "queryPlanner": {
    "winningPlan": {
      "stage": "IXSCAN",  // 使用索引扫描
      "indexName": "email_1"
    }
  },
  "executionStats": {
    "executionTimeMillis": 2,
    "totalDocsExamined": 1,  // 扫描的文档数
    "totalKeysExamined": 1,   // 扫描的索引键数
    "nReturned": 1            // 返回的文档数
  }
}

3.4.2 使用数据库分析器

// 启用分析器(级别2:记录所有操作)
db.setProfilingLevel(2);

// 查看分析日志
db.system.profile.find().sort({ ts: -1 }).limit(10);

// 查看慢查询(> 100ms)
db.system.profile.find({ millis: { $gt: 100 } }).sort({ ts: -1 });

// 停用分析器
db.setProfilingLevel(0);

3.4.3 使用索引建议器

// MongoDB 4.2+ 支持索引建议器
// 首先启用查询日志
db.setProfilingLevel(1, { slowms: 0 });

// 运行一段时间后,使用 mongodbatlas 或 MongoDB Compass 查看索引建议
// 或者使用以下命令查看慢查询
db.system.profile.find({ millis: { $gt: 0 } }).sort({ ts: -1 });

四、高级数据模型设计模式

4.1 分片策略

当数据量非常大时,需要考虑分片(Sharding)。

4.1.1 分片键选择

// 选择分片键的考虑因素:
// 1. 高基数(Cardinality):值分布广
// 2. 写入分布均匀:避免热点
// 3. 查询模式:经常作为查询条件

// 示例:用户集合按用户ID分片
sh.shardCollection("mydb.users", { _id: "hashed" });

// 示例:订单集合按用户ID分片(范围分片)
sh.shardCollection("mydb.orders", { user_id: 1 });

// 示例:日志集合按时间分片
sh.shardCollection("mydb.logs", { timestamp: 1 });

4.1.2 分片策略示例

// 1. 哈希分片(均匀分布)
sh.shardCollection("mydb.users", { _id: "hashed" });

// 2. 范围分片(有序数据)
sh.shardCollection("mydb.orders", { created_at: 1 });

// 3. 复合分片键
sh.shardCollection("mydb.events", { user_id: 1, timestamp: 1 });

4.2 时间序列数据设计

MongoDB 5.0+ 引入了时间序列集合,专门用于处理时间序列数据。

// 创建时间序列集合
db.createCollection("sensor_data", {
  timeseries: {
    timeField: "timestamp",
    metaField: "metadata",
    granularity: "hours"
  }
});

// 插入数据
db.sensor_data.insertOne({
  timestamp: new Date(),
  metadata: {
    sensor_id: "sensor-001",
    location: "warehouse-1"
  },
  temperature: 25.5,
  humidity: 60.2
});

// 查询时间序列数据
db.sensor_data.find({
  "metadata.sensor_id": "sensor-001",
  timestamp: {
    $gte: new Date("2023-05-10T00:00:00Z"),
    $lt: new Date("2023-05-11T00:00:00Z")
  }
});

4.3 变更数据捕获(CDC)设计

// 使用变更流(Change Streams)捕获数据变更
const pipeline = [
  {
    $match: {
      operationType: { $in: ["insert", "update", "delete"] }
    }
  }
];

const changeStream = db.collection("orders").watch(pipeline);

changeStream.on("change", (change) => {
  console.log("数据变更:", change);
  // 可以将变更发送到消息队列或触发其他操作
});

// 示例:监听特定集合的变更
const userChangeStream = db.users.watch([
  {
    $match: {
      "fullDocument.username": "alice"
    }
  }
]);

五、最佳实践与常见陷阱

5.1 最佳实践

  1. 设计前分析查询模式:了解应用的主要查询模式,根据查询设计数据模型
  2. 避免过度嵌套:嵌套文档深度不宜过深(通常不超过3层)
  3. 合理使用数组:数组大小应可控,避免无限增长
  4. 为常用查询创建索引:但不要创建过多索引(影响写入性能)
  5. 使用分片处理大数据:当单个集合超过100GB时考虑分片
  6. 定期清理过期数据:使用TTL索引自动清理
  7. 监控性能指标:关注查询响应时间、索引命中率等

5.2 常见陷阱

  1. 文档过大:单个文档超过16MB限制

    // 错误:文档过大
    db.collection.insertOne({
     _id: ObjectId("..."),
     data: new Array(1000000).fill("x")  // 可能超过16MB
    });
    
  2. 缺少索引:导致全表扫描

    // 错误:缺少索引的查询
    db.users.find({ email: "john@example.com" });  // 如果没有email索引,会扫描所有文档
    
  3. 过度使用 $where:性能极差

    // 错误:使用$where进行复杂计算
    db.users.find({
     $where: "this.age > 18 && this.orders.length > 5"
    });
    
  4. 不合理的分片键:导致热点问题

    // 错误:使用单调递增的字段作为分片键
    sh.shardCollection("mydb.logs", { timestamp: 1 });  // 所有新数据都写入最后一个分片
    
  5. 忽略连接池配置:导致连接数过多

    // 在应用中配置连接池
    const client = new MongoClient(uri, {
     maxPoolSize: 50,  // 最大连接数
     minPoolSize: 10,  // 最小连接数
     connectTimeoutMS: 30000
    });
    

六、实战案例:设计一个博客系统

6.1 需求分析

  • 用户管理:注册、登录、个人资料
  • 文章管理:创建、编辑、删除文章
  • 分类管理:文章分类
  • 评论系统:文章评论、回复
  • 标签系统:文章标签
  • 搜索功能:全文搜索

6.2 数据模型设计

// 1. 用户集合
db.createCollection("users", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["username", "email", "password_hash"],
      properties: {
        _id: { bsonType: "objectId" },
        username: { bsonType: "string", description: "用户名" },
        email: { bsonType: "string", description: "邮箱" },
        password_hash: { bsonType: "string", description: "密码哈希" },
        profile: {
          bsonType: "object",
          properties: {
            display_name: { bsonType: "string" },
            bio: { bsonType: "string" },
            avatar: { bsonType: "string" }
          }
        },
        role: { bsonType: "string", enum: ["user", "admin", "editor"] },
        created_at: { bsonType: "date" },
        updated_at: { bsonType: "date" }
      }
    }
  }
});

// 2. 文章集合
db.createCollection("articles", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["title", "slug", "author_id", "content", "status"],
      properties: {
        _id: { bsonType: "objectId" },
        title: { bsonType: "string" },
        slug: { bsonType: "string" },
        author_id: { bsonType: "objectId" },
        content: { bsonType: "string" },
        excerpt: { bsonType: "string" },
        category_ids: { bsonType: "array", items: { bsonType: "objectId" } },
        tags: { bsonType: "array", items: { bsonType: "string" } },
        status: { bsonType: "string", enum: ["draft", "published", "archived"] },
        published_at: { bsonType: "date" },
        created_at: { bsonType: "date" },
        updated_at: { bsonType: "date" }
      }
    }
  }
});

// 3. 分类集合
db.createCollection("categories", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["name", "slug"],
      properties: {
        _id: { bsonType: "objectId" },
        name: { bsonType: "string" },
        slug: { bsonType: "string" },
        description: { bsonType: "string" },
        parent_id: { bsonType: "objectId" },
        created_at: { bsonType: "date" }
      }
    }
  }
});

// 4. 评论集合
db.createCollection("comments", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["article_id", "user_id", "content"],
      properties: {
        _id: { bsonType: "objectId" },
        article_id: { bsonType: "objectId" },
        user_id: { bsonType: "objectId" },
        parent_comment_id: { bsonType: "objectId" },
        content: { bsonType: "string" },
        status: { bsonType: "string", enum: ["pending", "approved", "rejected"] },
        created_at: { bsonType: "date" },
        updated_at: { bsonType: "date" }
      }
    }
  }
});

6.3 索引设计

// 用户集合索引
db.users.createIndex({ username: 1 }, { unique: true });
db.users.createIndex({ email: 1 }, { unique: true });
db.users.createIndex({ "profile.display_name": 1 });
db.users.createIndex({ created_at: -1 });

// 文章集合索引
db.articles.createIndex({ slug: 1 }, { unique: true });
db.articles.createIndex({ author_id: 1 });
db.articles.createIndex({ category_ids: 1 });
db.articles.createIndex({ tags: 1 });
db.articles.createIndex({ status: 1, published_at: -1 });
db.articles.createIndex({ title: "text", content: "text", excerpt: "text" });  // 全文索引

// 分类集合索引
db.categories.createIndex({ slug: 1 }, { unique: true });
db.categories.createIndex({ parent_id: 1 });

// 评论集合索引
db.comments.createIndex({ article_id: 1, created_at: -1 });
db.comments.createIndex({ user_id: 1 });
db.comments.createIndex({ parent_comment_id: 1 });

6.4 常用查询示例

// 1. 获取用户及其文章
db.users.aggregate([
  {
    $match: { username: "alice" }
  },
  {
    $lookup: {
      from: "articles",
      localField: "_id",
      foreignField: "author_id",
      as: "articles"
    }
  },
  {
    $project: {
      username: 1,
      email: 1,
      "profile.display_name": 1,
      articles: {
        $slice: ["$articles", 5]  // 只返回最近5篇文章
      }
    }
  }
]);

// 2. 获取文章及其评论(分页)
db.articles.aggregate([
  {
    $match: { slug: "mongodb-data-model-design" }
  },
  {
    $lookup: {
      from: "comments",
      localField: "_id",
      foreignField: "article_id",
      as: "comments"
    }
  },
  {
    $unwind: { path: "$comments", preserveNullAndEmptyArrays: true }
  },
  {
    $lookup: {
      from: "users",
      localField: "comments.user_id",
      foreignField: "_id",
      as: "comments.user"
    }
  },
  {
    $unwind: { path: "$comments.user", preserveNullAndEmptyArrays: true }
  },
  {
    $group: {
      _id: "$_id",
      title: { $first: "$title" },
      content: { $first: "$content" },
      comments: {
        $push: {
          id: "$comments._id",
          content: "$comments.content",
          user: {
            username: "$comments.user.username",
            display_name: "$comments.user.profile.display_name"
          },
          created_at: "$comments.created_at"
        }
      }
    }
  },
  {
    $project: {
      title: 1,
      content: 1,
      comments: { $slice: ["$comments", 10] }  // 分页:每页10条评论
    }
  }
]);

// 3. 搜索文章(全文搜索)
db.articles.find({
  $text: { $search: "MongoDB 数据模型" }
}, {
  score: { $meta: "textScore" }
}).sort({ score: { $meta: "textScore" } }).limit(20);

// 4. 获取热门文章(按评论数)
db.articles.aggregate([
  {
    $match: { status: "published" }
  },
  {
    $lookup: {
      from: "comments",
      localField: "_id",
      foreignField: "article_id",
      as: "comments"
    }
  },
  {
    $addFields: {
      comment_count: { $size: "$comments" }
    }
  },
  {
    $sort: { comment_count: -1 }
  },
  {
    $limit: 10
  },
  {
    $project: {
      title: 1,
      slug: 1,
      comment_count: 1,
      published_at: 1
    }
  }
]);

七、总结

MongoDB 数据模型设计是一个需要综合考虑查询模式、数据关系、性能需求和扩展性的过程。没有一种设计适用于所有场景,关键在于理解业务需求并做出合理的权衡。

核心要点回顾:

  1. 嵌入 vs 引用:根据数据关系和查询模式选择
  2. 索引设计:为常用查询创建合适的索引,避免全表扫描
  3. 查询优化:使用投影、聚合管道和覆盖查询提高性能
  4. 分片策略:大数据量时考虑分片,选择合适的分片键
  5. 监控调优:持续监控性能,使用 explain() 和分析器优化查询

最终建议:

  • 设计前先分析:了解应用的主要查询模式
  • 从小开始:先设计简单模型,随着需求变化逐步优化
  • 测试验证:使用真实数据测试性能,避免过早优化
  • 保持灵活:MongoDB 的优势在于灵活性,不要过度设计
  • 持续学习:关注 MongoDB 新版本特性,不断优化数据模型

通过遵循这些原则和最佳实践,你可以设计出高效、可扩展的 MongoDB 数据模型,为应用提供强大的数据支持。记住,好的数据模型设计是应用成功的基础,值得投入时间和精力去精心设计。