题目
设计高效的分页查询方案
信息
- 类型:问答
- 难度:⭐⭐
考点
查询优化,索引设计,分页策略,聚合管道
快速回答
高效分页的核心方案:
- 避免使用
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自动清理旧文档提升分页效率
- 分桶模式:对超大数据集使用预聚合分桶存储(如按天分桶)