limit 1000000, 20慢是因为mysql必须扫描并丢弃前1000000行,无法跳过;优化需用延迟关联(子查询只查id再join)或游标分页(where id
为什么
LIMIT 1000000, 20会慢到不可接受MySQL 执行这个语句时,并不会“跳到第1000000行”,而是老老实实扫描前1000020行,逐行判断是否满足
WHERE条件、是否能进入排序结果集,再丢掉前1000000行——哪怕你只想要最后20条。如果排序字段没走索引,还会触发Using filesort;如果要查的字段不在索引里,每跳过一行都可能引发一次回表(随机IO)。偏移量上百万后,实际读取的数据量远超返回量,但99.99%都是白干。用覆盖索引 + 子查询 JOIN 能快多少?怎么写才不翻车
这个方案本质是把“找哪20行”和“取这20行的全部字段”拆开:子查询只走索引捞
id,外层用主键精准回查。但容易踩坑:
- 必须建联合索引,顺序得匹配查询逻辑,比如
WHERE status = 1 ORDER BY created_at DESC,索引就得是INDEX(status, created_at, id)(id放最后,确保子查询能直接覆盖)- 子查询里不能写
SELECT *或其他非索引字段,否则优化器会放弃覆盖索引JOIN条件必须是ON t1.id = t2.id这种等值连接,换成IN或范围条件,MySQL 5.7+ 也可能退化为全表扫描- 如果业务要查20+列,而联合索引太宽(比如含多个
VARCHAR(500)),B+ 树层级变深,定位反而更慢正确写法示例:
SELECT t1.* FROM orders t1 INNER JOIN ( SELECT id FROM orders WHERE status = 1 ORDER BY created_at DESC LIMIT 1000000, 20 ) t2 ON t1.id = t2.id;游标分页(
WHERE id )为什么更稳它不依赖行号,而是用上一页末尾的
id值做边界条件,起点可直接通过主键索引定位,时间复杂度稳定在O(log n)。但要注意:
- 必须有唯一、单调递增/递减的排序字段(如自增
id或created_at),且该字段已建索引- 数据删除或并发插入会导致“漏数据”或“重复数据”,比如用户刚翻到第3页,中间有人删了第2页末尾的记录,下一页就可能跳过一条
- 无法直接跳转到任意页码(比如“跳到第87页”),只适合“下一页/上一页”场景
- 如果排序依据是
created_at,需处理相同时间戳问题,建议组合(created_at, id)作为游标典型写法:
SELECT * FROM orders WHERE id <h3>哪些情况连游标分页都不好使</h3><p>当排序字段本身不具备唯一性、或业务强制要求支持任意页码跳转(比如后台管理系统的页码输入框)、或数据变更极其频繁且不允许漏/重时,上述两种方案都会受限。这时得考虑:</p>
- 前端限制最大页码(如禁止超过500页),配合提示“请用搜索或筛选缩小范围”
- 对历史数据归档,把冷数据移出主表,直接降低
offset的增长基数- 把分页逻辑从数据库下沉到应用层缓存(如 Redis 里预存热门分页结果),但需权衡一致性成本
- 用 Elasticsearch 替代 MySQL 做分页,尤其适合带全文检索、多条件聚合的场景
真正难的不是选哪种优化手段,而是意识到:深度分页本身是个反数据库设计的诉求——索引擅长定位,不擅长数数。越往后翻,越该怀疑是不是查询方式出了问题。












