LIMIT 1000000,20 慢是因为MySQL需扫描1000020行并回表1000020次,瓶颈在回表IO;应避免SELECT *,改用覆盖索引字段+联合索引优化,或用子查询先取ID再JOIN精准回表。

为什么 LIMIT 1000000, 20 会慢得离谱
MySQL 并不会跳过前 100 万行再读 20 行,而是老老实实扫描 1000020 行、回表 1000020 次,最后扔掉前 100 万行——哪怕你只想要 20 条数据。关键瓶颈在「回表」:二级索引查到主键后,还得一次次回到聚簇索引取完整行,IO 成倍放大。
SELECT * + LIMIT 必须避免直接使用
直接 SELECT * 会让优化器无法走覆盖索引,强制回表。哪怕加了 ORDER BY id,只要没限制查询字段,就大概率触发全字段回表。
- 必须明确写出需要的字段,比如
SELECT id, title, updated_at - 这些字段要全部包含在联合索引中(顺序需匹配
ORDER BY和WHERE) - 如果业务允许,把
updated_at换成id排序更稳——自增主键天然有序、无重复、无 NULL
用子查询先捞 ID,再关联原表取数据
核心思路是:让内层只走索引树(不回表),快速拿到目标页的 id 列表;外层用这些 id 精准回表,把回表次数从 100 万+ 降到 20 次。
SELECT u.* FROM users u INNER JOIN ( SELECT id FROM users WHERE status = 1 ORDER BY id LIMIT 1000000, 20 ) AS tmp ON u.id = tmp.id;
- 子查询
SELECT id ... LIMIT 1000000, 20只访问二级索引(或主键索引),不取其他字段,极快 - 外层
JOIN是等值匹配,u.id有主键索引,回表成本可控 - 注意:
status = 1这类过滤条件必须有对应索引,否则子查询本身就会扫全表
索引怎么建才真正生效
光写对 SQL 不够,索引结构决定它能不能跑得起来。不是随便加个 INDEX(status) 就行。
- 推荐建联合索引:
CREATE INDEX idx_status_id ON users(status, id) - 理由:
WHERE status = 1定位范围,ORDER BY id在该范围内天然有序,LIMIT可直接截断 - 如果排序字段是
updated_at,索引就得是(status, updated_at, id),且查询里SELECT的字段也得严格限定在这三者之内,才能覆盖 - 别依赖
id自带的主键索引去优化WHERE status—— 没过滤条件时它才高效
书签法(如 WHERE id > 1234567)确实更快,但它要求产品接受“只能下一页”的交互;而子查询法保留了跳页能力,代价是多一次嵌套和 JOIN。真要支持后台导出或管理端任意页码,这个折中很实际——但千万别忘了索引必须跟上,否则子查询自己就变慢查询了。











