侧边栏壁纸
博主头像
colo

欲买桂花同载酒

  • 累计撰写 1823 篇文章
  • 累计收到 0 条评论

设计高效的分页查询方案

2025-12-12 / 0 评论 / 4 阅读

题目

设计高效的分页查询方案

信息

  • 类型:问答
  • 难度:⭐⭐

考点

查询优化,索引设计,分页策略,聚合管道

快速回答

高效分页的核心方案:

  • 避免使用skip()处理大数据集
  • 使用范围查询(基于最后文档ID或时间戳)
  • 为排序字段创建复合索引
  • 聚合管道中使用$match + $sort + $limit组合
## 解析

问题场景

在商品管理系统中,商品集合products包含2000万文档,需要实现按创建时间倒序的分页查询(每页20条)。传统skip()方案在深度分页时性能急剧下降。

核心原理

  • skip()的性能缺陷:MongoDB必须扫描N条跳过文档才能返回结果,时间复杂度O(N)
  • 范围查询优势:通过索引直接定位数据起始点,时间复杂度O(1)
  • 索引覆盖:复合索引{createdAt: -1, _id: 1}可完全覆盖查询和排序

代码实现

// 最佳实践:基于最后文档的分页
const pageSize = 20;
let lastId = null; // 从请求参数获取上一页最后ID

const query = lastId 
  ? { $and: [
      { createdAt: { $lt: lastCreatedAt } }, 
      { _id: { $ne: ObjectId(lastId) } }
    ]} 
  : {};

db.products.find(query)
  .sort({ createdAt: -1, _id: -1 })
  .limit(pageSize);

// 聚合管道方案(复杂查询时)
db.products.aggregate([
  { $match: query },
  { $sort: { createdAt: -1, _id: -1 } },
  { $limit: pageSize },
  { $project: { name: 1, price: 1, createdAt: 1 } }
]);

最佳实践

  • 索引设计:创建db.products.createIndex({createdAt: -1, _id: -1})
  • 参数传递:客户端传递上一页最后文档的createdAt_id
  • 边界处理:第一页无lastId,最后一页返回空数组
  • 结果稳定性:添加_id排序避免相同createdAt导致的记录跳动

常见错误

  • ❌ 使用skip((page-1)*pageSize)处理深度分页
  • ❌ 未在排序字段建立索引导致内存排序
  • ❌ 忽略相同排序值导致的分页重复(如相同创建时间)
  • ❌ 在聚合管道中过早使用$skip阶段

扩展知识

  • 游标分页:适用于实时数据流,但无法跳转特定页码
  • count性能:避免countDocuments()统计大数据集,改用估算estimatedDocumentCount()
  • TTL索引:对日志类数据使用TTL自动清理旧文档提升分页效率
  • 分桶模式:对超大数据集使用预聚合分桶存储(如按天分桶)