游标分页可解决深分页性能问题,其核心是利用上一页最后一条记录的排序字段值(如id或created_at)作为下一页查询条件,使MySQL直接通过索引定位,避免扫描前N行。

直接用 LIMIT offset, size 做分页,在百万级数据下查第 1000 页(LIMIT 9990, 20)时,MySQL 仍要扫描前 9990 行才能跳到结果,性能断崖式下跌——这不是 PHP 的问题,是 SQL 执行逻辑决定的。
为什么 LIMIT M,N 在深分页时变慢
MySQL 不会“跳过前 M 行”,而是先按 ORDER BY 排出完整结果集,再截取第 M+1 到 M+N 行。如果没索引或排序列无索引,就会全表扫描;即使有索引,当 M 很大时,B+ 树也要从根反复向下定位、回溯,I/O 和 CPU 开销剧增。
- 现象:
EXPLAIN显示rows值接近总行数,Extra出现Using filesort或Using temporary - 典型错误写法:
SELECT * FROM orders ORDER BY created_at DESC LIMIT 50000, 20 - 关键点:慢不是因为 PHP,而是这条语句本身在 MySQL 层就不可扩展
用游标分页(Cursor-based Pagination)替代 offset
核心思路:不依赖“第几页”,改用“上一页最后一条的排序字段值”作为下一页起点,让 MySQL 能直接利用索引定位,跳过所有中间行。
- 前提:必须有确定顺序的、带索引的排序字段(如主键
id或时间戳created_at) - 正确写法(升序示例):
SELECT * FROM orders WHERE id > 123456 ORDER BY id ASC LIMIT 20 - 降序兼容写法(需注意方向):
SELECT * FROM orders WHERE created_at - PHP 中传递的不是页码,而是上一页最后一条的
id或created_at值,避免整型溢出或时间精度丢失
索引必须匹配查询模式
光有索引不够,结构必须贴合你的 WHERE + ORDER BY + LIMIT 组合,否则 MySQL 无法跳过排序阶段。
立即学习“PHP免费学习笔记(深入)”;
- 错误索引:
INDEX(created_at)—— 单独建在排序字段上,但WHERE created_at + <code>ORDER BY created_at DESC可用;若加了其他WHERE条件(如status = 'paid'),这个索引就失效 - 推荐组合索引:
ALTER TABLE orders ADD INDEX idx_status_created (status, created_at),适用于WHERE status = 'paid' AND created_at - 覆盖索引优化(减少回表):
SELECT order_id, user_id, amount FROM orders WHERE ...—— 若只查这三列,可建INDEX(status, created_at, order_id, user_id, amount),让 MySQL 直接从索引取值
PHP 层需配合处理边界与一致性
游标分页不是把 SQL 换掉就完事,PHP 代码要主动应对数据动态变化带来的错位。
- 新数据插入导致“漏数据”:用户翻页间隙有新记录插入在当前游标值之前,下一页会跳过它 → 解决办法:前端传游标值时,服务端用
SELECT ... FOR UPDATE或加唯一时间戳字段(如created_at, id双条件)提高精度 - 删除导致“重复数据”:某条被删后,下一页可能重复返回原游标后第一条 → 建议在分页接口中强制要求客户端传上一页最后一条的完整主键值(如
last_id=123456&last_created=2025-03-15...),WHERE 条件写成WHERE (created_at, id) (MySQL 支持元组比较) - 不要在 PHP 里用
array_slice()模拟分页:大数据量下一次性 fetch_all() 会爆内存,必须靠数据库层分页
最容易被忽略的是:游标值必须来自数据库实际返回的字段,不能靠 PHP 计算或拼接。哪怕只是多了一毫秒、少了一个微秒,都可能导致下一页空结果或重复。稳定分页的本质,是把“位置”交给索引和存储引擎去保证,而不是靠应用层计数。



















