mysql limit offset 越往后越慢是因为需先跳过offset行再取数据,导致全表扫描;应改用游标分页,以排序字段值为起点避免跳过大量数据。

MySQL LIMIT OFFSET 为什么越往后越慢
因为数据库必须先扫描并跳过前 OFFSET 行,再取 LIMIT 行——数据量大时,跳过几十万行就是全表扫描的代价。不是“取10条慢”,而是“跳过999990条”慢。
- 真实场景:用户翻到第1000页(
OFFSET 99990),哪怕有索引,MySQL 仍需定位到第99990个索引位置,逐条计数推进 - 复合索引有效但有前提:必须覆盖
ORDER BY字段 + 查询字段,且WHERE条件能命中索引最左前缀 - 如果
ORDER BY id DESC,但id是主键,LIMIT 10 OFFSET 100000依然慢——InnoDB 的聚簇索引遍历无法跳过中间节点 - 用
EXPLAIN看rows值,如果远大于LIMIT数,基本就是 OFFSET 陷阱
用游标分页(Cursor-based Pagination)替代 OFFSET
不依赖行号,改用上一页最后一条记录的排序字段值作为下一页起点,避免跳过大量数据。
- 适用前提:排序字段必须唯一、非空、有索引(如
created_at+id组合,或直接用主键id) - 查询写法:把
WHERE id > ? ORDER BY id ASC LIMIT 10替代LIMIT 10 OFFSET 1000 - 注意方向一致性:如果前端按时间倒序展示,后端应存上一页最小的
created_at,再查WHERE created_at - 不能直接用于“跳转任意页”,只适合“下一页/上一页”连续浏览;搜索、筛选后首次加载仍需一次带
OFFSET的查询(但仅第一次)
postgresql 的 cursor 分页更友好?
是的,但不是语法糖,而是底层支持更成熟的游标机制和更精准的索引扫描策略。
-
DECLARE my_cursor CURSOR for select ... ORDER BY id;配合FETCH NEXT 10 FROM my_cursor可复用游标状态,适合长连接分批拉取 - 更常用的是基于
WHERE id > $last_id的无状态游标,和 MySQL 逻辑一致,但 PostgreSQL 的索引跳跃能力略强,尤其在WHERE + ORDER BY同字段时 - 注意:如果排序字段有重复值(比如多个记录
created_at相同),必须加二级排序(如ORDER BY created_at DESC, id DESC),否则可能漏行或重复 - PostgreSQL 的
OFFSET同样慢,别以为换数据库就自动优化
什么时候还不得不硬扛 OFFSET?
当业务明确要求“跳转到第 N 页”且 N 不大(比如 ≤ 50),或数据总量小(LIMIT OFFSET 仍是最快落地方案。
- 加缓存:对固定页码(如热门榜单第1页、第3页)做 redis 缓存,key 为
page:rank:1,避免反复查库 - 限制页码上限:后端校验
page * size ,超出直接返回空或提示“已到末尾”,防止恶意刷大 offset - 用物化视图或汇总表预计算:比如每日凌晨跑任务,把“用户昨日活跃排名前1000”固化到
daily_rank_snapshot表,分页查这张轻表 - 不要在事务里嵌套深分页查询——锁范围随
OFFSET增大而扩大,容易阻塞写操作
游标分页看着简单,但排序字段选错、重复值没兜底、前端传参校验松,三者任一出问题,分页就错位。真上线前,拿百万级测试数据跑一遍第100页、第500页的边界 case。