为什么 OFFSET 越大,LIMIT 查询越慢
MySQL 的
LIMIT offset, size在底层需要先扫描并跳过前
offset行,再取
size行。当
offset达到几十万甚至百万级时,即使有索引,MySQL 仍需逐行计数、判断是否满足偏移条件,I/O 和 CPU 开销陡增。这不是“查得慢”,而是“不得不扫那么多行”。 执行
EXPLAIN会看到
rows值接近
offset + size,而非仅
size覆盖索引失效:若
SELECT *或非索引字段参与查询,会导致回表,进一步放大延迟 主从延迟敏感:大 offset 查询常在从库长时间运行,拖慢复制位点
用游标分页(Cursor-based Pagination)替代 OFFSET
核心思路是不依赖行号,改用上一页最后一条记录的排序字段值作为下一页起点。要求排序字段严格唯一且有索引(如自增
id或带唯一约束的
created_at, id组合)。 第一页:
SELECT * FROM orders WHERE status = 'paid' ORDER BY id ASC LIMIT 50第二页(假设上页最大
id是 12345):
SELECT * FROM orders WHERE status = 'paid' AND id > 12345 ORDER BY id ASC LIMIT 50必须去掉
OFFSET,WHERE 条件走索引范围扫描,执行计划中
rows≈
size注意方向一致性:升序分页用
>,降序用
;混合排序(如 <code>ORDER BY created_at DESC, id DESC)需同时比较两个字段
复合排序字段没唯一性?加 id
补足
常见场景是按时间分页:
ORDER BY created_at DESC,但同一秒可能有多条记录,直接用
created_at做游标会导致漏数据或重复。 安全写法:
ORDER BY created_at DESC, id DESC,游标条件写成
WHERE created_at索引必须包含全部排序字段:
INDEX(created_at, id),顺序与
ORDER BY一致 避免在 WHERE 中对排序字段做函数操作(如
DATE(created_at)),否则索引失效
物理删除不现实时,慎用 SQL_CALC_FOUND_ROWS
它曾被用来查总条数以渲染分页控件,但实际会强制 MySQL 扫全表/全索引,性能比带
OFFSET还差。现代应用基本应弃用。 替代方案一:前端只显示“下一页”,不显示总页数(如 Twitter、GitHub) 替代方案二:用近似值——查
SELECT COUNT(*)改为
SELECT TABLE_ROWS FROM information_schema.TABLES(误差可接受,尤其对大表) 替代方案三:业务允许时,在写入侧维护统计缓存(如 Redis 中的
orders:count),异步更新
真实线上分页卡顿,八成不是 SQL 写错了,而是还在用
LIMIT 1000000, 20这种逻辑。游标分页改造成本低,效果立竿见影,但前提是排序字段能锚定唯一位置——这点容易被忽略,也最难补救。
