thinkphp深分页变慢主因是mysql的offset需扫描并丢弃前n行,非框架问题;应改用游标分页,以唯一有序字段(如id)为条件,where id > ? + order by id asc + limit,避免全表跳过。

ThinkPHP 的 paginate() 在深分页时变慢,不是框架写得差,而是它默认走 MySQL 的 LIMIT offset, size,而 MySQL 执行这种语句必须扫描并丢弃前 offset 行——数据量一过百万,OFFSET 50000 就已经要扫 5 万 + size 行,CPU 和 I/O 都在空转。
为什么 OFFSET 越大查询越卡
MySQL 并不会“跳过”前 N 行,而是老老实实读出来、排序(如果没覆盖索引)、再扔掉。尤其当 SELECT * 配合 ORDER BY created_at 且该字段无唯一性时,还会触发 Using filesort 或 Using temporary。
- 表有 100 万行,查第 1000 页(每页 25 条),
OFFSET是 24975 → MySQL 至少读取 25000 行才返回 25 条 - 若
ORDER BY字段没索引,或索引不覆盖查询字段,就会回表,I/O 成倍放大 - 高并发下多个深分页请求堆积,可能直接拖垮数据库连接池
ThinkPHP 里怎么绕开 paginate() 的 offset 陷阱
别调用 paginate(),直接用 Db::name() 或模型的链式查询拼游标逻辑。核心是:用上一页最后一条记录的排序字段值做 WHERE 条件,不再依赖页码。
- 必须确保排序字段唯一、非空、有索引(推荐
id或带索引的created_at) - 首次请求不带游标:
->where('id', '>', 0)或直接省略 WHERE - 后续请求传
cursor=12345(上一页最后一条的id),查->where('id', '>', 12345) - 方向要统一:升序就始终
ORDER BY id ASC,降序就始终ORDER BY id DESC,不能混用 - 前端拿到结果后,取
end($list)['id']作为next_cursor,下一次请求带上
游标分页在 ThinkPHP 模型里怎么封装才安全
把游标逻辑收口到模型方法里,避免控制器重复拼 SQL,也方便校验和复用。
- 用
$this->getPk()动态获取主键名,别硬写'id' -
$cursor值必须类型校验:整数字段用intval(),时间字段需确认格式(如Y-m-d H:i:s) - 返回结构里必须包含
next_cursor,且为空时显式设为null,前端好判断是否到底 - 不要在模型里写死表名,用
$this->getTable();也不要用Db::name()绕过模型,否则丢失事件、自动转换等能力 - 慎用
created_at单字段游标——毫秒级重复会导致漏数据,建议组合created_at+id(如WHERE (created_at, id) > (?, ?))
哪些场景不适合强行上游标分页
游标分页快,但它是有代价的:它放弃总条数、放弃随机跳页。真要支持“跳到第 382 页”,就得接受性能妥协,或换方案。
- 用户能手动输入页码(比如后台管理列表)→ 游标无法满足,得用延迟关联(
JOIN (SELECT id FROM ... LIMIT offset, size))或按时间分区查 - 数据高频删除或 ID 重用(如软删后 ID 回收)→ 游标值可能失效,导致漏或重
- 排序字段本身不稳定(如
status变更频繁)→ 同一游标值多次查询结果不一致 - 需要精确总数展示(如“共 12486 条”)→ 游标分页不该、也不能提供这个,COUNT(*) 本身已成瓶颈
最易被忽略的一点:游标值不是“页码”,它本质是数据快照位置。前端传错、后端没校验、排序字段索引缺失——这三个点只要踩中一个,分页就悄无声息地漏数据或重复,而且很难被日志发现。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











