直接用 skip() + limit() 分页危险,因 skip 会强制扫描跳过所有前置文档,导致 cpu 和响应时间线性恶化;应改用游标分页,按索引字段排序并查 limit+1 条判断下一页。

为什么直接用 skip() + limit() 在 MongoDB 分页里很危险
因为文档数量增长后,skip() 会强制扫描并跳过前面所有匹配文档,哪怕你只要第 100 页的 20 条——MongoDB 仍得从头数到 99 × 20 = 1980 条。CPU 和响应时间线性恶化,5000 页之后基本卡死。
真实场景中,用户翻到第 30 页就明显变慢,后台日志频繁出现 executionTimeMillis > 2000,这不是代码写得不够“优雅”,是架构层面踩了坑。
- 适用于小数据量(总文档
- 不能用于用户可任意输入页码的接口(比如
?page=9999) - 配合
sort()使用时,必须确保排序字段有索引,否则全表扫描加倍恶化
用游标分页(cursor-based pagination)替代页码分页
核心思路:不记“第几页”,只记“从哪条继续往后取”。每次返回结果末尾的 _id(或业务唯一递增字段),下一页请求带上这个值作为起点。
PHP 中实际就是把 find() 的 $filter 从空对象换成带条件的查询,例如:
// 第一页(无 cursor)
$cursor = null;
// 后续请求传入上一页最后一条的 _id 字符串,如:'65a1b2c3d4e5f67890123456'
if ($cursor) {
$filter['$gt'] = new MongoDB\BSON\ObjectId($cursor);
}
$result = $collection->find($filter, [
'sort' => ['created_at' => 1],
'limit' => 20,
]);
- 必须按同一字段排序(推荐
created_at或带索引的递增字段),且升序/降序固定 -
_id是 ObjectId,默认按插入时间递增,适合做游标,但注意它不是绝对时间戳(毫秒级精度,但生成逻辑含机器标识) - 不要用字符串比较
_id,必须用MongoDB\BSON\ObjectId实例,否则类型不匹配查不到
PHP 翻页函数怎么封装才不埋雷
一个可用的最小翻页函数,要隐藏游标细节、自动处理边界、返回结构化响应,而不是裸露 find() 结果。
关键点不是“怎么查”,而是“怎么让调用方不用操心游标生命周期”:
function paginateFromCursor(
MongoDB\Collection $collection,
array $filter = [],
string $sortField = 'created_at',
int $limit = 20,
?string $cursor = null
): array {
$query = $filter;
if ($cursor) {
$query[$sortField] = ['$gt' => new MongoDB\BSON\ObjectId($cursor)];
}
$options = [
'sort' => [$sortField => 1],
'limit' => $limit + 1, // 多查 1 条,判断是否有下一页
];
$results = iterator_to_array($collection->find($query, $options));
$hasNext = count($results) > $limit;
if ($hasNext) {
array_pop($results); // 剔除多出的那条
}
$nextCursor = $hasNext ? (string)$results[array_key_last($results)][$sortField] : null;
return [
'data' => $results,
'next_cursor' => $nextCursor,
'has_more' => $hasNext,
];
}
- 永远查
$limit + 1条,而不是依赖count()—— 避免额外一次查询,也避开大数据量下count()慢的问题 - 返回的
next_cursor必须是字符串((string)$objId),前端才能安全传递,别返回 ObjectId 对象 - 不要在函数里做
toArray()以外的数据加工(如格式化时间),那是业务层的事;保持函数专注“分页逻辑”
当必须支持传统页码(如管理后台)时怎么折中
如果产品硬性要求支持 ?page=123,又无法说服改造成游标,那就限制可访问范围,而不是放任穿透。
例如:只允许 page ≤ 100,超出则返回 400 或重定向到第 100 页,并记录告警日志。比让它拖垮数据库强得多。
- 用
ceil($total / $limit)计算最大合法页码,但total本身要用count(['limit' => 10000])加上限,防止 count 全表 - 对
page参数做强校验:is_numeric($page) && $page >= 1 && $page - 缓存高频页码(如前 10 页)的结果,用
cacheKey = "page_{$page}_{$limit}_{$hashOfFilter}",但注意失效策略
游标分页不是“更高级的写法”,它是面对海量文档时唯一能落地的分页方式。页码只是 UI 层的幻觉,后端不该为幻觉买单。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











