引言

MongoDB作为一款流行的NoSQL文档型数据库,以其灵活的数据模型和强大的扩展能力在现代应用开发中占据重要地位。然而,这种灵活性也带来了设计上的挑战。与传统关系型数据库不同,MongoDB的数据模型设计需要充分考虑查询模式、数据访问频率和业务逻辑,以实现最佳性能和可维护性。

本文将从文档结构设计、索引策略、查询优化、数据分片等多个维度,全面探讨MongoDB数据模型设计的最佳实践,并通过具体示例展示如何在实际项目中应用这些原则。

1. 文档结构设计基础

1.1 嵌入式文档 vs 引用关系

MongoDB支持两种主要的数据关系建模方式:嵌入式文档引用关系

嵌入式文档将相关数据直接存储在父文档中,适合数据访问模式一致、数据量适中的场景。

// 嵌入式文档示例:订单与订单项
{
  "_id": ObjectId("60a7b8c9d1e2f3a4b5c6d7e8"),
  "orderNumber": "ORD-2023-001",
  "customerId": ObjectId("5f9d1a2b3c4d5e6f7a8b9c0d"),
  "orderDate": ISODate("2023-05-15T10:30:00Z"),
  "status": "completed",
  "totalAmount": 299.99,
  "items": [
    {
      "productId": ObjectId("60a7b8c9d1e2f3a4b5c6d7e9"),
      "productName": "Wireless Mouse",
      "quantity": 2,
      "unitPrice": 49.99,
      "subtotal": 99.98
    },
    {
      "productId": ObjectId("60a7b8c9d1e2f3a4b5c6d7ea"),
      "productName": "Mechanical Keyboard",
      "quantity": 1,
      "unitPrice": 199.99,
      "subtotal": 199.99
    }
  ],
  "shippingAddress": {
    "street": "123 Main St",
    "city": "San Francisco",
    "state": "CA",
    "zipCode": "94102",
    "country": "USA"
  },
  "paymentMethod": "credit_card",
  "createdAt": ISODate("2023-05-15T10:30:00Z"),
  "updatedAt": ISODate("2023-05-15T10:35:00Z")
}

引用关系通过ID引用其他文档,适合数据量大、需要独立访问或更新的场景。

// 引用关系示例:用户与订单分离
// 用户集合
{
  "_id": ObjectId("5f9d1a2b3c4d5e6f7a8b9c0d"),
  "username": "john_doe",
  "email": "john@example.com",
  "profile": {
    "firstName": "John",
    "lastName": "Doe",
    "phone": "+1-555-0123"
  },
  "addresses": [
    {
      "type": "shipping",
      "street": "123 Main St",
      "city": "San Francisco",
      "state": "CA",
      "zipCode": "94102",
      "country": "USA"
    }
  ],
  "createdAt": ISODate("2023-01-10T08:00:00Z")
}

// 订单集合(引用用户)
{
  "_id": ObjectId("60a7b8c9d1e2f3a4b5c6d7e8"),
  "orderNumber": "ORD-2023-001",
  "customerId": ObjectId("5f9d1a2b3c4d5e6f7a8b9c0d"), // 引用用户ID
  "orderDate": ISODate("2023-05-15T10:30:00Z"),
  "status": "completed",
  "totalAmount": 299.99,
  "items": [
    {
      "productId": ObjectId("60a7b8c9d1e2f3a4b5c6d7e9"),
      "productName": "Wireless Mouse",
      "quantity": 2,
      "unitPrice": 49.99
    }
  ],
  "createdAt": ISODate("2023-05-15T10:30:00Z")
}

1.2 选择策略的决策矩阵

场景特征 推荐嵌入式 推荐引用关系
数据访问模式 经常一起查询 独立查询为主
数据量 适中(<100MB) 大量(>100MB)
更新频率 低频更新 高频独立更新
数据生命周期 相同 不同
数据一致性要求 强一致性 最终一致性可接受

实际案例:电商系统设计

// 方案A:嵌入式设计(适合小型电商)
// 产品文档包含所有变体
{
  "_id": ObjectId("60a7b8c9d1e2f3a4b5c6d7e9"),
  "name": "Wireless Mouse",
  "category": "Electronics",
  "brand": "TechBrand",
  "description": "High-precision wireless mouse",
  "variants": [
    {
      "sku": "WM-BLACK",
      "color": "black",
      "size": "standard",
      "price": 49.99,
      "stock": 150,
      "images": ["img1.jpg", "img2.jpg"]
    },
    {
      "sku": "WM-WHITE",
      "color": "white",
      "size": "standard",
      "price": 49.99,
      "stock": 75,
      "images": ["img3.jpg", "img4.jpg"]
    }
  ],
  "reviews": [
    {
      "userId": ObjectId("5f9d1a2b3c4d5e6f7a8b9c0d"),
      "rating": 5,
      "comment": "Excellent product!",
      "createdAt": ISODate("2023-05-10T14:20:00Z")
    }
  ]
}

// 方案B:引用关系设计(适合大型电商)
// 产品主文档
{
  "_id": ObjectId("60a7b8c9d1e2f3a4b5c6d7e9"),
  "name": "Wireless Mouse",
  "category": "Electronics",
  "brand": "TechBrand",
  "description": "High-precision wireless mouse",
  "createdAt": ISODate("2023-01-01T00:00:00Z")
}

// 产品变体集合
{
  "_id": ObjectId("60a7b8c9d1e2f3a4b5c6d7ea"),
  "productId": ObjectId("60a7b8c9d1e2f3a4b5c6d7e9"),
  "sku": "WM-BLACK",
  "color": "black",
  "size": "standard",
  "price": 49.99,
  "stock": 150,
  "images": ["img1.jpg", "img2.jpg"]
}

// 评论集合
{
  "_id": ObjectId("60a7b8c9d1e2f3a4b5c6d7eb"),
  "productId": ObjectId("60a7b8c9d1e2f3a4b5c6d7e9"),
  "userId": ObjectId("5f9d1a2b3c4d5e6f7a8b9c0d"),
  "rating": 5,
  "comment": "Excellent product!",
  "createdAt": ISODate("2023-05-10T14:20:00Z")
}

1.3 文档大小优化技巧

MongoDB文档大小限制为16MB,需要合理控制文档大小:

// 优化前:文档过大风险
{
  "_id": ObjectId("60a7b8c9d1e2f3a4b5c6d7e8"),
  "orderNumber": "ORD-2023-001",
  "items": [
    // 假设每个订单有1000个商品项
    // 每个商品项包含详细描述、图片URL等
  ],
  "customerProfile": {
    // 包含完整的用户信息
  },
  "paymentDetails": {
    // 包含完整的支付信息
  }
}

// 优化后:合理拆分
{
  "_id": ObjectId("60a7b8c9d1e2f3a4b5c6d7e8"),
  "orderNumber": "ORD-2023-001",
  "customerId": ObjectId("5f9d1a2b3c4d5e6f7a8b9c0d"),
  "orderDate": ISODate("2023-05-15T10:30:00Z"),
  "status": "completed",
  "totalAmount": 299.99,
  "itemCount": 3, // 只存储数量,不存储详细项
  "summary": {
    "topProducts": ["Wireless Mouse", "Mechanical Keyboard"],
    "categories": ["Electronics", "Accessories"]
  },
  "createdAt": ISODate("2023-05-15T10:30:00Z")
}

// 详细订单项存储在单独集合
{
  "_id": ObjectId("60a7b8c9d1e2f3a4b5c6d7e9"),
  "orderId": ObjectId("60a7b8c9d1e2f3a4b5c6d7e8"),
  "items": [
    {
      "productId": ObjectId("60a7b8c9d1e2f3a4b5c6d7ea"),
      "productName": "Wireless Mouse",
      "quantity": 2,
      "unitPrice": 49.99,
      "subtotal": 99.98
    }
  ],
  "createdAt": ISODate("2023-05-15T10:30:00Z")
}

2. 索引策略与查询优化

2.1 索引类型选择

MongoDB提供多种索引类型,每种都有其适用场景:

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

// 2. 复合索引
// 注意:复合索引的字段顺序很重要
db.orders.createIndex({ "customerId": 1, "orderDate": -1 })
// 适合查询:db.orders.find({ customerId: ObjectId("...") }).sort({ orderDate: -1 })

// 3. 唯一索引
db.users.createIndex({ "email": 1 }, { unique: true })

// 4. TTL索引(自动过期)
db.sessions.createIndex(
  { "createdAt": 1 }, 
  { expireAfterSeconds: 3600 } // 1小时后自动删除
)

// 5. 文本索引(全文搜索)
db.products.createIndex(
  { "name": "text", "description": "text" },
  { weights: { name: 10, description: 5 } }
)

// 6. 地理空间索引
db.places.createIndex({ "location": "2dsphere" })

// 7. 哈希索引(用于等值查询)
db.sessions.createIndex({ "sessionId": "hashed" })

2.2 复合索引设计原则

最左前缀原则:MongoDB可以使用复合索引的前缀部分进行查询。

// 创建复合索引
db.orders.createIndex({ "customerId": 1, "orderDate": 1, "status": 1 })

// 有效查询(使用索引)
db.orders.find({ customerId: ObjectId("...") })
db.orders.find({ customerId: ObjectId("..."), orderDate: { $gte: ISODate("2023-01-01") } })
db.orders.find({ customerId: ObjectId("..."), orderDate: ISODate("2023-05-15"), status: "completed" })

// 无效查询(无法使用索引)
db.orders.find({ orderDate: ISODate("2023-05-15") })
db.orders.find({ status: "completed" })
db.orders.find({ orderDate: ISODate("2023-05-15"), status: "completed" })

2.3 索引选择策略

// 场景:用户查询订单
// 查询模式1:按用户ID查询所有订单
db.orders.find({ customerId: ObjectId("...") })

// 查询模式2:按用户ID和日期范围查询
db.orders.find({ 
  customerId: ObjectId("..."), 
  orderDate: { $gte: ISODate("2023-01-01"), $lte: ISODate("2023-12-31") } 
})

// 查询模式3:按用户ID、日期和状态查询
db.orders.find({ 
  customerId: ObjectId("..."), 
  orderDate: { $gte: ISODate("2023-01-01") },
  status: "completed"
})

// 最佳索引设计
db.orders.createIndex({ 
  "customerId": 1, 
  "orderDate": 1, 
  "status": 1 
})

// 如果查询模式有变化,考虑创建多个索引
// 索引1:针对查询模式1和2
db.orders.createIndex({ "customerId": 1, "orderDate": 1 })

// 索引2:针对查询模式3
db.orders.createIndex({ "customerId": 1, "status": 1, "orderDate": 1 })

2.4 索引性能监控

// 1. 查看索引使用情况
db.orders.aggregate([
  { $indexStats: { } }
])

// 2. 使用explain()分析查询性能
db.orders.find({ customerId: ObjectId("...") }).explain("executionStats")

// 3. 查看慢查询日志(需要在mongod配置中启用)
// mongod.conf
// systemLog:
//   destination: file
//   path: /var/log/mongodb/mongod.log
//   logAppend: true
// operationProfiling:
//   mode: slowOp
//   slowOpThresholdMs: 100
//   slowOpSampleRate: 1.0

// 4. 使用db.currentOp()查看当前操作
db.currentOp({
  "active": true,
  "secs_running": { "$gt": 10 }
})

3. 查询优化技巧

3.1 查询模式优化

// 1. 避免全表扫描
// 低效查询
db.users.find({ "profile.age": { $gte: 18 } }) // 无索引,全表扫描

// 高效查询(创建索引)
db.users.createIndex({ "profile.age": 1 })
db.users.find({ "profile.age": { $gte: 18 } })

// 2. 使用投影减少数据传输
// 低效查询(返回所有字段)
db.users.find({ "profile.age": { $gte: 18 } })

// 高效查询(只返回需要的字段)
db.users.find(
  { "profile.age": { $gte: 18 } },
  { "username": 1, "email": 1, "profile.age": 1, "_id": 0 }
)

// 3. 分页优化
// 传统分页(性能问题)
db.orders.find({ customerId: ObjectId("...") })
  .sort({ orderDate: -1 })
  .skip(1000)
  .limit(100)

// 优化分页(使用游标)
const cursor = db.orders.find({ customerId: ObjectId("...") })
  .sort({ orderDate: -1 })
  .limit(100)

// 第一页
const page1 = cursor.toArray()

// 第二页(使用上一页最后一条记录的日期)
const lastDate = page1[page1.length - 1].orderDate
const page2 = db.orders.find({
  customerId: ObjectId("..."),
  orderDate: { $lt: lastDate }
}).sort({ orderDate: -1 }).limit(100)

// 4. 使用聚合管道优化复杂查询
// 传统方式(多次查询)
const orders = db.orders.find({ customerId: ObjectId("...") }).toArray()
const productIds = orders.flatMap(order => order.items.map(item => item.productId))
const products = db.products.find({ _id: { $in: productIds } }).toArray()

// 聚合管道(单次查询)
db.orders.aggregate([
  { $match: { customerId: ObjectId("...") } },
  { $unwind: "$items" },
  { $group: { 
      _id: null, 
      totalAmount: { $sum: "$items.subtotal" },
      productIds: { $addToSet: "$items.productId" }
    } 
  },
  { $lookup: {
      from: "products",
      localField: "productIds",
      foreignField: "_id",
      as: "products"
    }
  }
])

3.2 聚合管道优化

// 场景:统计每个用户的订单数量和总金额
// 低效方式(多次查询)
const users = db.users.find().toArray()
const userStats = users.map(user => {
  const orders = db.orders.find({ customerId: user._id }).toArray()
  const totalAmount = orders.reduce((sum, order) => sum + order.totalAmount, 0)
  return {
    userId: user._id,
    username: user.username,
    orderCount: orders.length,
    totalAmount: totalAmount
  }
})

// 高效聚合管道
db.orders.aggregate([
  {
    $group: {
      _id: "$customerId",
      orderCount: { $sum: 1 },
      totalAmount: { $sum: "$totalAmount" }
    }
  },
  {
    $lookup: {
      from: "users",
      localField: "_id",
      foreignField: "_id",
      as: "user"
    }
  },
  {
    $unwind: "$user"
  },
  {
    $project: {
      userId: "$_id",
      username: "$user.username",
      orderCount: 1,
      totalAmount: 1,
      _id: 0
    }
  },
  {
    $sort: { totalAmount: -1 }
  },
  {
    $limit: 10
  }
])

// 优化技巧:使用$facet进行多维度统计
db.orders.aggregate([
  { $match: { orderDate: { $gte: ISODate("2023-01-01") } } },
  {
    $facet: {
      // 统计1:按状态分组
      byStatus: [
        { $group: { _id: "$status", count: { $sum: 1 } } }
      ],
      // 统计2:按月份分组
      byMonth: [
        {
          $group: {
            _id: { $month: "$orderDate" },
            count: { $sum: 1 },
            totalAmount: { $sum: "$totalAmount" }
          }
        }
      ],
      // 统计3:热门商品
      topProducts: [
        { $unwind: "$items" },
        { $group: { _id: "$items.productId", count: { $sum: "$items.quantity" } } },
        { $sort: { count: -1 } },
        { $limit: 5 }
      ]
    }
  }
])

3.3 读写分离与副本集配置

// 1. 配置副本集(3节点)
// 启动3个mongod实例
// mongod --replSet rs0 --port 27017 --dbpath /data/db1
// mongod --replSet rs0 --port 27018 --dbpath /data/db2
// mongod --replSet rs0 --port 27019 --dbpath /data/db3

// 初始化副本集
rs.initiate({
  _id: "rs0",
  members: [
    { _id: 0, host: "localhost:27017", priority: 2 },
    { _id: 1, host: "localhost:27018", priority: 1 },
    { _id: 2, host: "localhost:27019", priority: 1, hidden: true }
  ]
})

// 2. 读写分离配置
// 在应用层配置读偏好
const MongoClient = require('mongodb').MongoClient;
const uri = "mongodb://localhost:27017,localhost:27018,localhost:27019/?replicaSet=rs0";

const client = new MongoClient(uri, {
  readPreference: 'secondaryPreferred', // 优先从secondary读取
  maxPoolSize: 10,
  minPoolSize: 2
});

// 3. 写关注(Write Concern)
// 确保数据写入多数节点
db.orders.insertOne(
  { customerId: ObjectId("..."), orderDate: new Date(), totalAmount: 99.99 },
  { writeConcern: { w: "majority", wtimeout: 5000 } }
)

// 4. 读关注(Read Concern)
// 确保读取的数据是已提交的
db.orders.find(
  { customerId: ObjectId("...") },
  { readConcern: { level: "majority" } }
)

4. 数据分片策略

4.1 分片键选择

// 1. 选择分片键的原则
// - 高基数(cardinality):唯一值多
// - 写分布均匀:避免热点
// - 查询模式匹配:经常作为查询条件

// 2. 示例:用户集合分片
// 好的分片键:用户ID(高基数,查询常用)
sh.shardCollection("app.users", { "_id": "hashed" })

// 3. 示例:订单集合分片
// 问题:按customerId分片可能导致热点(大客户订单多)
// 解决方案:使用复合分片键
sh.shardCollection("app.orders", { "customerId": 1, "orderDate": 1 })

// 4. 示例:日志集合分片
// 按时间分片(时间序列数据)
sh.shardCollection("app.logs", { "timestamp": 1 })

// 5. 分片配置
sh.enableSharding("app")
sh.shardCollection("app.users", { "_id": "hashed" })
sh.shardCollection("app.orders", { "customerId": 1, "orderDate": 1 })
sh.shardCollection("app.logs", { "timestamp": 1 })

4.2 分片管理

// 1. 查看分片状态
sh.status()

// 2. 添加分片
sh.addShard("localhost:27018")
sh.addShard("localhost:27019")

// 3. 均衡器管理
// 启用/禁用均衡器
sh.setBalancerState(true) // 启用
sh.setBalancerState(false) // 禁用

// 4. 分片范围管理
// 查看分片范围
db.getSiblingDB("config").chunks.find({ ns: "app.orders" })

// 5. 手动拆分分片
sh.splitAt("app.orders", { customerId: ObjectId("..."), orderDate: ISODate("2023-06-01") })

5. 数据一致性与事务

5.1 MongoDB事务支持

// MongoDB 4.0+支持多文档事务
const session = client.startSession();

try {
  session.startTransaction();
  
  // 操作1:更新订单状态
  await db.collection('orders').updateOne(
    { _id: ObjectId("...") },
    { $set: { status: "shipped" } },
    { session }
  );
  
  // 操作2:减少库存
  await db.collection('products').updateOne(
    { _id: ObjectId("...") },
    { $inc: { stock: -2 } },
    { session }
  );
  
  // 操作3:记录物流信息
  await db.collection('shipments').insertOne(
    {
      orderId: ObjectId("..."),
      trackingNumber: "123456789",
      shippedAt: new Date()
    },
    { session }
  );
  
  await session.commitTransaction();
} catch (error) {
  await session.abortTransaction();
  throw error;
} finally {
  session.endSession();
}

5.2 数据一致性策略

// 1. 使用原子操作
// 避免先读取再更新的竞态条件
// 低效且不安全
const doc = db.collection.findOne({ _id: id });
doc.counter++;
db.collection.updateOne({ _id: id }, { $set: { counter: doc.counter } });

// 高效且安全
db.collection.updateOne(
  { _id: id },
  { $inc: { counter: 1 } }
)

// 2. 使用findAndModify(原子操作)
const result = db.collection.findOneAndUpdate(
  { _id: id, status: "pending" },
  { $set: { status: "processing" } },
  { returnDocument: "after" }
)

// 3. 使用乐观锁
db.collection.updateOne(
  { _id: id, version: currentVersion },
  { 
    $set: { 
      status: "updated",
      version: currentVersion + 1 
    } 
  }
)

6. 数据生命周期管理

6.1 TTL索引自动清理

// 1. 会话数据自动过期
db.sessions.createIndex(
  { "lastAccess": 1 },
  { expireAfterSeconds: 3600 } // 1小时后自动删除
)

// 2. 临时数据清理
db.tempData.createIndex(
  { "createdAt": 1 },
  { expireAfterSeconds: 86400 } // 24小时后自动删除
)

// 3. 日志数据按时间清理
db.logs.createIndex(
  { "timestamp": 1 },
  { expireAfterSeconds: 2592000 } // 30天后自动删除
)

6.2 数据归档策略

// 1. 按时间归档
// 将旧数据移动到归档集合
const archiveDate = new Date();
archiveDate.setMonth(archiveDate.getMonth() - 6); // 6个月前的数据

const oldOrders = db.orders.find({ orderDate: { $lt: archiveDate } });
const archivedOrders = oldOrders.map(order => ({
  ...order,
  archivedAt: new Date(),
  originalCollection: "orders"
}));

db.ordersArchive.insertMany(archivedOrders);
db.orders.deleteMany({ orderDate: { $lt: archiveDate } });

// 2. 使用MongoDB的Time Series集合(MongoDB 5.0+)
db.createCollection(
  "sensorData",
  {
    timeseries: {
      timeField: "timestamp",
      metaField: "metadata",
      granularity: "hours"
    },
    expireAfterSeconds: 2592000 // 30天后自动删除
  }
)

7. 监控与性能调优

7.1 性能监控工具

// 1. 使用db.serverStatus()获取服务器状态
db.serverStatus()

// 2. 使用db.collection.stats()查看集合统计
db.orders.stats()

// 3. 使用db.currentOp()查看当前操作
db.currentOp({
  "active": true,
  "secs_running": { "$gt": 5 }
})

// 4. 使用MongoDB Compass可视化分析
// 安装MongoDB Compass并连接到数据库
// 查看查询性能、索引使用情况、数据分布

// 5. 使用MongoDB Atlas监控(云版本)
// Atlas提供实时监控、性能建议、警报等功能

7.2 慢查询分析

// 1. 启用慢查询日志
// mongod.conf
// operationProfiling:
//   mode: slowOp
//   slowOpThresholdMs: 100
//   slowOpSampleRate: 1.0

// 2. 分析慢查询日志
// 使用mongodump导出日志
// 使用MongoDB Compass或第三方工具分析

// 3. 使用explain()分析查询计划
db.orders.find({ customerId: ObjectId("...") }).explain("executionStats")

// 4. 优化建议
// - 检查是否使用了索引
// - 检查扫描文档数量
// - 检查返回文档数量
// - 检查索引选择性

8. 实际案例:电商系统设计

8.1 完整数据模型

// 用户集合
db.users.createIndex({ "email": 1 }, { unique: true })
db.users.createIndex({ "username": 1 })
db.users.createIndex({ "createdAt": 1 })

// 产品集合
db.products.createIndex({ "category": 1 })
db.products.createIndex({ "brand": 1 })
db.products.createIndex({ "price": 1 })
db.products.createIndex({ "name": "text", "description": "text" })

// 订单集合
db.orders.createIndex({ "customerId": 1, "orderDate": -1 })
db.orders.createIndex({ "status": 1 })
db.orders.createIndex({ "orderNumber": 1 }, { unique: true })

// 购物车集合
db.carts.createIndex({ "userId": 1 }, { unique: true })
db.carts.createIndex({ "items.productId": 1 })

// 评论集合
db.reviews.createIndex({ "productId": 1, "createdAt": -1 })
db.reviews.createIndex({ "userId": 1 })

8.2 常见查询优化

// 1. 用户订单历史查询
// 优化前:多次查询
const userOrders = db.orders.find({ customerId: userId }).toArray()
const orderDetails = userOrders.map(order => {
  const items = order.items.map(item => {
    const product = db.products.findOne({ _id: item.productId })
    return { ...item, product }
  })
  return { ...order, items }
})

// 优化后:聚合管道
db.orders.aggregate([
  { $match: { customerId: userId } },
  { $unwind: "$items" },
  {
    $lookup: {
      from: "products",
      localField: "items.productId",
      foreignField: "_id",
      as: "items.product"
    }
  },
  { $unwind: "$items.product" },
  {
    $group: {
      _id: "$_id",
      orderNumber: { $first: "$orderNumber" },
      orderDate: { $first: "$orderDate" },
      totalAmount: { $first: "$totalAmount" },
      items: { $push: "$items" }
    }
  },
  { $sort: { orderDate: -1 } }
])

// 2. 产品搜索和过滤
// 优化前:多次查询
const products = db.products.find({
  category: "Electronics",
  price: { $gte: 50, $lte: 500 },
  brand: { $in: ["Apple", "Samsung"] }
}).toArray()

// 优化后:复合索引 + 聚合
db.products.createIndex({ "category": 1, "price": 1, "brand": 1 })

db.products.aggregate([
  {
    $match: {
      category: "Electronics",
      price: { $gte: 50, $lte: 500 },
      brand: { $in: ["Apple", "Samsung"] }
    }
  },
  {
    $lookup: {
      from: "reviews",
      localField: "_id",
      foreignField: "productId",
      as: "reviews"
    }
  },
  {
    $addFields: {
      avgRating: { $avg: "$reviews.rating" },
      reviewCount: { $size: "$reviews" }
    }
  },
  { $sort: { avgRating: -1 } },
  { $limit: 20 }
])

9. 最佳实践总结

9.1 文档设计原则

  1. 根据查询模式设计文档结构:优先考虑如何查询数据,而不是如何存储数据
  2. 控制文档大小:避免超过16MB限制,必要时拆分数据
  3. 使用嵌入式文档:当数据访问模式一致且数据量适中时
  4. 使用引用关系:当数据量大、需要独立访问或更新时
  5. 保持数据一致性:使用事务或原子操作确保数据一致性

9.2 索引策略原则

  1. 为查询创建索引:分析查询模式,创建合适的索引
  2. 遵循最左前缀原则:复合索引的字段顺序很重要
  3. 避免过度索引:每个索引都会增加写操作开销
  4. 定期审查索引:删除未使用的索引
  5. 使用覆盖索引:让查询只使用索引,避免回表

9.3 查询优化原则

  1. 使用投影减少数据传输:只返回需要的字段
  2. 避免全表扫描:确保查询使用索引
  3. 优化分页:使用游标或范围查询替代skip/limit
  4. 使用聚合管道:复杂查询使用聚合管道
  5. 读写分离:使用副本集实现读写分离

9.4 数据管理原则

  1. 数据生命周期管理:使用TTL索引自动清理过期数据
  2. 数据归档:定期归档旧数据
  3. 监控与调优:持续监控性能,定期优化
  4. 备份与恢复:制定完善的备份策略
  5. 安全策略:启用认证、授权、加密等安全措施

10. 常见陷阱与解决方案

10.1 陷阱1:过度嵌套

// 问题:过度嵌套导致文档过大
{
  "user": {
    "profile": {
      "personal": {
        "name": {
          "first": "John",
          "last": "Doe"
        },
        "contact": {
          "email": "john@example.com",
          "phone": "+1-555-0123"
        }
      }
    }
  }
}

// 解决方案:扁平化结构
{
  "firstName": "John",
  "lastName": "Doe",
  "email": "john@example.com",
  "phone": "+1-555-0123"
}

10.2 陷阱2:忽略索引选择性

// 问题:为低选择性字段创建索引
db.users.createIndex({ "gender": 1 }) // 只有2个值,选择性低

// 解决方案:为高选择性字段创建索引
db.users.createIndex({ "email": 1 }) // 唯一值多,选择性高

10.3 陷阱3:不当使用$in查询

// 问题:$in查询包含大量值
db.orders.find({ 
  customerId: { $in: [id1, id2, id3, ..., id10000] } 
})

// 解决方案:分批查询或使用聚合
// 分批查询
const batchSize = 1000;
for (let i = 0; i < customerIds.length; i += batchSize) {
  const batch = customerIds.slice(i, i + batchSize);
  const orders = db.orders.find({ customerId: { $in: batch } }).toArray();
  // 处理orders
}

// 或使用聚合
db.orders.aggregate([
  { $match: { customerId: { $in: customerIds } } },
  // 其他处理
])

结语

MongoDB数据模型设计是一个持续优化的过程,需要根据业务需求、查询模式和性能要求不断调整。本文从文档结构设计、索引策略、查询优化、数据分片等多个维度提供了全面的指导,并通过具体示例展示了最佳实践的应用。

记住,没有”一刀切”的解决方案。每个应用都有其独特的需求,最佳实践需要结合具体场景进行调整。建议在实际项目中持续监控性能,定期审查数据模型,并根据业务变化进行优化。

通过遵循这些最佳实践,您可以构建出高性能、可扩展且易于维护的MongoDB应用,充分发挥MongoDB在现代应用开发中的优势。