offset分页卡顿因mysql需扫描前n行,游标分页通过排序字段值(如created_at+id)避免全扫,要求字段唯一、非空、有索引,配合逐行处理与总数缓存提升性能。

为什么直接用 OFFSET 分页会卡顿
当数据量超过几十万行,SELECT * FROM posts LIMIT 100000, 20 这类基于 OFFSET 的分页会越来越慢。MySQL 仍要扫描前 100000 行才能跳到目标位置,索引也无法完全规避全扫描。
用游标分页(Cursor-based Pagination)替代 LIMIT OFFSET
核心是「不依赖行号,而依赖上一页最后一条记录的排序字段值」。要求排序字段(如 created_at 或 id)有唯一性、非空、有索引。
- 第一页查:
SELECT id, title, created_at FROM posts ORDER BY created_at DESC, id DESC LIMIT 21(多取 1 条用于判断是否有下一页) - 第二页传入上一页最后一条的
created_at和id:SELECT id, title, created_at FROM posts WHERE created_at - 必须给
(created_at, id)建联合索引,否则性能无保障 - 不能跳页(比如从第 1 页直接到第 100 页),但对“加载更多”场景完全够用
PDO::fetch() 和内存控制要配合分页逻辑
大结果集不等于大内存占用——关键在 fetch 方式和是否一次性 fetchAll()。
- 用
$stmt->fetch()逐行处理,而不是$stmt->fetchAll()把全部 10 万行载入 PHP 数组 - 如果模板里需要总条数,避免
SELECT COUNT(*)全表扫;可缓存总数(如 Redis 存posts:count),或用近似值(SHOW TABLE STATUS的Rows字段) - 分页参数(如
cursor)必须做严格校验:if (!preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $cursor)) die('invalid cursor');
什么时候还不得不回退到 LIMIT OFFSET
搜索结果页、后台管理页这类需要跳转任意页码的场景,游标分页不适用。此时应:
- 限制最大页码(如
min($page, 200)),避免用户输入?page=10000) - 对
WHERE条件加覆盖索引,让 MySQL 尽量只走索引不回表 - 考虑用 Elasticsearch 或 MySQL 8.0+ 的
WINDOW函数预计算分页锚点,但复杂度陡增
游标分页不是银弹,它把「跳页自由」换成了「线性加载稳定」。真要支持跳页又扛住百万数据,得接受引入额外服务或预聚合——这点容易被低估。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











