LIMIT offset, size会逐行扫描并丢弃前offset行,导致IO和CPU开销线性增长;应改用游标分页或延迟关联优化。

OFFSET不是跳转,是逐行计数丢弃
数据库执行 LIMIT 10 OFFSET 100000 时,并不会“定位到第100001行”,而是从索引起点开始,挨个读取、解析、计数、再丢弃前100000行——哪怕你只要20条数据,它也得处理至少100020条记录。B+树索引能加速查找某个key,但无法直接寻址“第N条排序后的位置”。
常见错误现象:EXPLAIN 中 rows 显示值 ≈ OFFSET + LIMIT,Extra 列出现 Using filesort 或空值(说明没走覆盖索引);高并发下 Buffer Pool 被大量无效页占满,拖慢其他查询。
- 即使
ORDER BY id有主键索引,B+树仍需遍历大量叶子节点才能“数够”10万行 - 若排序字段无索引,或
WHERE条件与ORDER BY不满足最左前缀匹配,直接退化为全表扫描+临时文件排序 -
OFFSET传负数或非整数时,MySQL 报错ERROR: OFFSET must not be negative,但业务层未校验会导致500或静默失败
回表让深分页的IO灾难翻倍
二级索引只存排序字段和主键,LIMIT offset, size 查出主键后,还得根据这些主键逐一回聚簇索引取完整行——OFFSET 100000 意味着10万次随机磁盘IO(或Buffer Pool查找),远超顺序扫描成本。
强制覆盖索引可缓解:例如只 SELECT id, title FROM posts ORDER BY created_at DESC LIMIT 100000, 20,避免回表;延迟关联(Deferred Join)更彻底:先用索引查出主键列表,再 JOIN 原表批量取详情。
- 别迷信“加索引就快”——得看
EXPLAIN里是index还是range访问类型,type=ALL说明索引完全失效 - SQL Server 中,
INCLUDE列必须包含所有SELECT字段,否则仍会触发回表 - MySQL 8.0 不支持
INCLUDE,只能靠联合索引把常用查询字段全包进去
游标分页为什么能绕过OFFSET瓶颈
游标分页把“第N页”转成“比上一页最后一条记录更大的下一批”,MySQL 可用索引直接二分定位起点,全程走 range 扫描,执行时间稳定在毫秒级,与总数据量无关。
关键陷阱在排序字段唯一性:单用 created_at DESC,高并发下同一毫秒多条记录,WHERE created_at 会漏掉同时间戳的其他行。安全写法必须用复合排序+复合比较:<code>ORDER BY created_at DESC, id DESC + WHERE (created_at, id) 。
- MySQL 8.0+ 和 PostgreSQL 支持行构造器语法;SQLite 不支持,得拆成
created_at - 前端必须透传上一页末尾的
last_id或(last_created_at, last_id),不能由后端拼接字符串,防SQL注入 - 键集分页不支持跳页,用户输
page=5000时,后端不能硬算,应拒绝或重定向到首页
什么时候还不得不硬扛OFFSET
后台导出、数据稽核、审计报表等场景,确实需要跳转任意页码,且无法预知目标位置——这时游标分页不适用,只能优化OFFSET本身。
优先建覆盖索引(含 WHERE 字段、ORDER BY 字段、SELECT 所有列),SQL Server 用 INCLUDE,MySQL 用联合索引;再配合延迟关联减少回表;最后考虑加缓存层(如Redis预存分页结果),但要注意数据一致性窗口。
真正容易被忽略的是:统计信息过期时,SQL Server 查询优化器可能放弃使用索引,改用更“稳妥”但更慢的执行计划——定期更新统计信息比加索引还重要。

















