LIMIT 1000000,10会扫描1000010行再丢弃前100万行,因MySQL需逐行计数定位,若无覆盖索引还会频繁回表,导致IO和CPU开销剧增。

为什么 LIMIT 1000000, 10 会扫 100 万行才出结果
MySQL 执行 LIMIT offset, size 时,并不会跳过前 offset 行,而是从头开始逐行扫描、计数,直到累计够 offset + size 行,再丢弃前 offset 行。这意味着 LIMIT 1000000, 10 实际要读取并处理 1000010 行数据。
更糟的是:如果查询带 WHERE 和 ORDER BY,且排序字段没走覆盖索引,MySQL 还得反复回表——每扫一行就去主键索引捞一次完整记录。IO 和 CPU 开销指数级上升。
常见错误现象包括:
-
EXPLAIN显示rows值极大(比如 >50 万),但Extra里还有Using filesort或Using temporary - 同一语句在测试库秒出,在生产库查 5 秒以上,且
offset每加 10 万,耗时明显递增 - 慢日志里频繁出现
Query_time: 3.212且Rows_examined: 451350这类组合
用子查询先捞 ID,再 JOIN 取全量字段
核心思路是把“查全量记录”拆成两步:第一步只走索引(甚至覆盖索引),快速定位目标 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;
用 WHERE id > ? 替代大 offset,但只适用于顺序翻页
当业务允许“一页页往下刷”(如 APP 下拉加载),直接用上一页最后的 id 作为下一页起点,性能飞跃。此时 MySQL 只需在主键 B+ 树叶子节点链表上往后走 size 条,完全规避偏移计算。
关键约束和陷阱:
- 必须确保
ORDER BY字段是**严格单调唯一**的(推荐用主键id,慎用update_time——可能有重复值) - 不能跳页:用户从第 1 页直接点第 500 页,此法失效;需配合游标(cursor)参数传递,后端不存状态
- 示例:
SELECT id, name, balance FROM account WHERE id > 6800000 ORDER BY id LIMIT 10;
- 如果排序依据是时间字段,且存在重复,可追加主键去重:
ORDER BY update_time DESC, id DESC,对应查询条件也要补上:WHERE (update_time, id)
为什么复合索引对子查询优化至关重要
子查询 SELECT id FROM t WHERE x = ? ORDER BY y LIMIT m,n 能否走索引、是否产生临时表,完全取决于索引设计。若 WHERE 和 ORDER BY 涉及不同字段,又没建好复合索引,子查询本身就会变慢,优化就白搭。
正确做法:
- 把
WHERE条件字段放在复合索引前面,ORDER BY字段放后面,例如:KEY idx_update_time_id (update_time, id) - 如果
SELECT列全在索引里(如只查id),就是覆盖索引,连主键回表都省了 - 避免在子查询里用函数或表达式,比如
WHERE DATE(update_time) = '2025-05-10'会让索引失效 - 用
EXPLAIN验证子查询是否走了索引:type应为range或ref,key显示实际使用的索引名,Extra不含Using filesort或Using temporary
真正难的不是写出优化 SQL,而是让所有分页路径都收敛到同一个索引策略上——尤其是当 WHERE 条件动态拼接、前端传参不固定时,很容易漏建某个组合的索引,导致某类请求突然变慢。


















