Offset分页越查越慢是因为MySQL需扫描并丢弃前N行;ThinkPHP需手写游标分页,用唯一递增字段(如id)作游标,WHERE id > ? + ORDER BY id ASC + LIMIT,避免全表跳过。

为什么Offset分页在ThinkPHP里越查越慢
因为OFFSET本质是让数据库跳过前N行再取数据,当OFFSET值达到几十万甚至百万时,MySQL仍要扫描并丢弃前面所有匹配行——哪怕你只想要10条。ThinkPHP默认的paginate()就是基于LIMIT offset, size,不改底层逻辑,加索引也救不了。
ThinkPHP 6+ 怎么手动实现游标分页(cursor-based)
游标分页不依赖行号,而是用上一页最后一条记录的排序字段值(比如id或created_at)作为下一页的起点,查询变成WHERE id > ? ORDER BY id ASC LIMIT 20,避免全表跳过。
实操建议:
- 必须有**唯一、递增、非空**的排序字段(推荐
id或带时间戳的create_time),不能用name这类可能重复的字段 - ThinkPHP不内置游标分页,需手写查询:先查当前页数据,再用
max(id)或last()->id生成下一页游标 - 前端传参不是
page=2,而是cursor=12345(上一页最后一条的id值) - 注意边界:首次请求无游标,用
WHERE id > 0或直接省略;反向翻页(上一页)需用ORDER BY id DESC+MAX(id)反推
示例(获取下一页):
立即学习“PHP免费学习笔记(深入)”;
$cursor = $request->param('cursor', 0, 'intval');
$list = Db::name('article')
->where('id', '>', $cursor)
->order('id ASC')
->limit(20)
->select();
$nextCursor = $list ? end($list)['id'] : null;
用paginate()强行套游标?别试了
ThinkPHP的paginate()硬编码了LIMIT ?,?逻辑,没法注入WHERE id > ?条件。有人想用withTotal(false)再自己算总数,但分页器内部仍会执行COUNT(*),而游标分页本就不该、也不能精确返回总页数。
常见错误现象:
- 把
cursor塞进page参数,结果paginate(20, false, ['page' => $cursor])被当成页码,触发LIMIT ($cursor-1)*20, 20,彻底跑偏 - 试图用
->offset(0)覆盖,但底层SQL已生成,无效 - 忽略排序稳定性:如果
ORDER BY create_time且存在毫秒级重复,会导致同游标查出不同数据或漏数据
游标分页的代价和妥协点
它快,但放弃两件事:总条数、随机跳页(比如直接跳到“第87页”)。用户只能“下一页/上一页”,适合信息流、日志、订单列表等场景。
关键细节容易被忽略:
-
WHERE条件必须和排序字段一起走索引,比如INDEX (status, id),若查询带WHERE status=1,则ORDER BY id才能走索引 - 游标值必须原样透传,不能四舍五入、转字符串截断、或被框架自动过滤(如
intval()对超大ID会溢出) - 删除中间数据不会导致“断页”,但新插入的数据可能出现在上一页末尾——这是游标分页的正常行为,不是bug
真要支持跳页,得保留两套分页逻辑:游标用于滚动加载,传统Offset用于后台管理页,别混用。



















