paginate()卡死是因为count(*)+limit offset,size导致mysql扫描丢弃前n行,应禁用总数统计改用simple分页,或在百万级数据且连续浏览场景下严格按索引字段用游标分页。

因为 paginate() 底层强制执行 COUNT(*) + LIMIT offset, size,MySQL 必须扫描并丢弃前 N 行——这不是 PHP 慢,是 SQL 本身在大偏移下就不可扩展。
为什么 paginate() 一查第100页就卡死
典型现象:第 90 页开始响应从 200ms 暴涨到 3s+,CPU 占用拉满,日志无报错但接口超时。根本原因有两个叠加:
-
COUNT(*) FROM table WHERE ...:哪怕加了索引,优化器仍可能走全表扫描,尤其当WHERE条件选择性低(比如status = 1占 80% 数据) -
LIMIT 99980, 20:MySQL 要定位到第 99980 行起点,必须逐行读取、计数、丢弃,IO 和 CPU 双重压垮 - 两者合起来,TP 默认分页在 10 万行以上深度翻页时基本失效,和 ThinkPHP 版本(5.1/6.x/8.1)、PHP 版本(8.4/8.5.5)都无关
怎么用 simple() 快速止血
如果前端只要「上一页 / 下一页」按钮,不需要「共 XX 页」或跳转任意页码,这是最简单有效的绕过方案:
- 控制器里写:
$list = Db::name('order')->where('user_id', $uid)->paginate(20, false); - 模板里渲染:
{$list->simple()->appends(request()->param())->render()} - 必须显式加
appends(),否则翻页时搜索参数(如status=1)会丢失 - 效果:跳过
COUNT(*),只查数据 + 判断是否有下一页,响应从秒级降到毫秒级
什么时候必须上游标分页
当数据稳定在百万级、且用户行为是连续浏览(如后台订单流、日志列表),游标分页是唯一能保证性能的方案:
- SQL 写法示例(按
id ASC):SELECT * FROM user WHERE id > 100500 AND status = 1 ORDER BY id ASC LIMIT 20 -
ORDER BY方向必须和WHERE条件严格一致(>对应ASC,对应 <code>DESC),写反直接退化为全表扫描 - 字段必须有索引,且非空、唯一、有序——主键
id最稳妥,create_time需配合id做联合条件防重复 - 不能混用
paginate()的参数(如simple、query),游标分页天然不支持总页数和跳页
真正容易被忽略的点是:游标分页快不快,最终取决于 MySQL 能不能用索引直接定位起始行——如果 WHERE 条件里混了没索引的字段,或者 ORDER BY 字段没索引,前面所有优化都白搭。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











