游标分页能绕过limit offset扫描开销,因mysql执行limit 1000000,10时需顺序读取1000010条并丢弃前100万条,而游标分页用上一页末尾字段值(如id)构造where条件直接定位,仅扫描limit行,时间复杂度从o(offset+size)降至o(size)。

直接用 paginate() 查第 500 页,MySQL 就得扫描前 9990 行再丢弃——这不是 PHP 函数写得不好,是 SQL 层面的硬伤。必须绕开 LIMIT offset, size 这条路。
关闭 totalCount + 改用 simple 分页
这是最快见效的改动,适合前端不依赖“共 XX 页”、只提供“下一页”按钮的场景。
- 控制器中调用:
$list = Db::name('order')->where('user_id', $uid)->paginate(20, false);—— 第二个参数false会跳过COUNT(*)查询 - 模板中渲染必须用
{$list->simple()->appends(request()->param())->render()},否则翻页时?keyword=abc这类参数全丢 - 注意:simple 模式只返回“是否有下一页”,不支持跳转任意页码;如果用户手动输
?page=1000,TP 会静默 fallback 到第一页,不报错也不警告
用游标分页替代 offset 分页
当数据量稳定在百万级、且用户行为是连续翻页(如后台日志、订单流),游标分页是唯一靠谱方案。它不看页码,只认上一页最后一条的锚点值。
- 首选游标字段是自增
id:首次请求查SELECT * FROM order ORDER BY id ASC LIMIT 20,拿到最后一条的id = 12345;下一页查WHERE id > 12345 ORDER BY id ASC LIMIT 20 - 若用时间字段,必须搭配唯一性保障,比如复合索引
(create_time, id),查询写成WHERE (create_time, id) > (?, ?)(MySQL 8.0+)或拆解为WHERE create_time > ? OR (create_time = ? AND id > ?)(兼容老版本) - 客户端传来的游标值必须强校验:整型就
intval(),字符串就过滤非数字字符,绝不能拼进 SQL —— 必须走预处理绑定
索引必须覆盖 WHERE + ORDER BY + 游标字段
游标分页快不快,最终卡在 MySQL 能不能用索引“一步到位”完成定位、排序、取数。光有索引不够,顺序和字段都得对。
- 查
WHERE user_id = ? ORDER BY create_time DESC,索引就得建在(user_id, create_time),且create_time字段不能为NULL(MySQL 可能直接弃用该索引) - 执行
EXPLAIN SELECT * FROM order WHERE user_id = 123 ORDER BY create_time DESC LIMIT 20,确认type不是ALL,key显示你建的索引名 - 如果查询里还有
status = 1这类条件,索引字段顺序就得调整为(user_id, status, create_time),否则索引失效
为什么简单加 limit 和 offset 不行
因为 MySQL 的执行逻辑不是“跳到第 N 行”,而是“从头开始读,边读边数,数够 N 行再开始取”。哪怕你只要 20 条,offset 是 100 万,它就得回表读 1000020 行完整数据,再扔掉前 100 万。
- IO 放大:磁盘读了 100 万行,只用了 20 行
- CPU 浪费:每行都要做主键回表、字段解析、内存拷贝
- 锁竞争:在事务里执行,可能长时间持有行锁或间隙锁
- PHP 层无解:无论你换什么框架、调什么参数,只要 SQL 是
LIMIT 999990, 20,瓶颈就在数据库这一侧
真正容易被忽略的是游标字段的可靠性——别拿 created_at 当唯一锚点,时区、批量插入、业务回拨都会让它重复或倒流;id 看似简单,但如果用的是 UUID 或分库分表后的逻辑 ID,也得先确认它全局有序。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











