limit 1000000, 10会扫描1000010行,因mysql需逐行计数并丢弃前100万行;若order by未走覆盖索引,每行还需回表,导致io与cpu开销线性增长。

为什么LIMIT 1000000, 10会扫100万行才出结果
MySQL执行LIMIT offset, size时,不会跳过前offset行,而是从头开始逐行扫描、计数,直到累计读满offset + size行,再丢弃前offset行。所以LIMIT 1000000, 10实际要读取并处理1000010行数据。
更关键的是:如果查询带ORDER BY且该字段没走覆盖索引,MySQL每扫一行就要回表一次——主键索引树上找ID,再回聚簇索引捞整行。IO和CPU开销随offset线性增长,不是“慢一点”,是“指数级恶化”。
常见错误现象包括:
-
EXPLAIN显示rows值极大(比如 >50万),但Extra里还有Using filesort或Using temporary - 同一语句在测试库秒出,在生产库查5秒以上,且
offset每加10万,耗时明显递增 - 慢日志里频繁出现
Query_time: 3.212且Rows_examined: 451350这类组合
延迟关联:最通用的SQL层优化写法
核心思路是把“查全量记录”拆成两步:第一步只走索引(甚至覆盖索引),快速捞出目标id;第二步用这些id精准回表。避免大范围扫描和无效回表。
实操建议如下:
- 子查询必须只
SELECT id,且ORDER BY字段需有索引(最好是主键或联合索引最左前缀) - 外层用
JOIN比IN更可靠——MySQL 8.0以前不支持IN子句里用LIMIT - 复合条件(如
WHERE update_time > '2025-01-01')必须下推到子查询中,否则外层JOIN会放大结果集
示例语句:
SELECT a.* FROM account a INNER JOIN ( SELECT id FROM account WHERE update_time > '2025-01-01' ORDER BY id LIMIT 1000000, 10 ) b ON a.id = b.id;
游标分页:性能最强,但业务要配合
适用于“一页页往下刷”的场景(如APP下拉加载、Feed流),直接用上一页最后一条记录的id(或时间戳+主键组合)作为下一页起点。MySQL只需在主键B+树叶子节点链表上往后走size条,完全规避偏移计算。
实操要点:
- 第一页:先查
SELECT * FROM orders ORDER BY id DESC LIMIT 10,拿到最后一条的id(比如9990) - 第二页:改用
SELECT * FROM orders WHERE id - 若排序字段非主键(如
create_time),需用复合条件避免重复/漏数据:WHERE create_time
优势是无论翻到第几亿条,性能都和第一页一样快;缺点是无法支持随机跳页(如直接点第1000页)。
子查询定位起始ID:支持跳页但要注意边界
当用户必须能输入任意页码(比如后台管理页),可用子查询先查出第M条的id,再用WHERE id >=取数据:
SELECT * FROM orders WHERE id >= (SELECT id FROM orders ORDER BY id LIMIT 1000000, 1) ORDER BY id LIMIT 10;
这个写法比延迟关联少一次JOIN,但有两个易踩的坑:
- 子查询
LIMIT 1000000, 1返回的是第1000001个id,但如果该id对应多条记录(比如id不是主键、或有重复排序值),外层可能查出超过10条 - 如果排序字段存在大量相同值(如
status字段只有0/1),ORDER BY status, id必须写全,否则结果不稳定 - MySQL 5.7对子查询中
LIMIT的支持较弱,建议升级到8.0+并确认执行计划是否走索引
真正难的不是写出某条优化SQL,而是判断当前业务能不能接受游标分页、要不要为跳页牺牲一致性、以及有没有漏掉WHERE条件下推——这些细节一错,优化就变负优化。











