limit offset, size在千万级表中变慢是因为mysql需扫描offset+size行,逐行排序、回表并丢弃前offset行,导致全索引扫描、大量随机i/o和回表开销。

为什么 LIMIT offset, size 在千万级表里会越来越慢
MySQL 执行 LIMIT 1000000, 20 时,并不会跳到第 1000001 行再读,而是从头扫描 1000020 行,逐行计数、排序、回表,最后丢弃前 1000000 行。这导致三个硬伤:全索引扫描 + 大量回表 + 随机 I/O。尤其当 SELECT * 遇上无覆盖索引时,每丢弃一行,就多一次聚簇索引查找——2000 万数据扫到第 100 万页,等于做了百万次随机磁盘寻道。
延迟关联写法必须满足的 3 个前提条件
延迟关联(INNER JOIN 子查询)不是套个 SQL 就能生效,它依赖底层索引行为:
-
ORDER BY字段必须有索引,且该索引要能覆盖WHERE条件最左前缀(比如WHERE status = 1+ORDER BY created_at DESC,索引得建为(status, created_at)) - 子查询里只能
SELECT id(或其它索引覆盖字段),不能带SELECT *或非索引字段,否则照样触发回表 - 外层
JOIN的主表别名(如t1)和子查询别名(如t2)必须明确,且ON t1.id = t2.id中的id是主键或唯一键,否则可能产生笛卡尔积
示例有效写法:
SELECT t1.* FROM t_order t1 INNER JOIN ( SELECT id FROM t_order WHERE status = 1 ORDER BY created_at DESC LIMIT 1000000, 20 ) t2 ON t1.id = t2.id;
游标分页(Keyset Pagination)怎么避免“上一页末尾值重复”陷阱
用 WHERE id > last_seen_id 替代 LIMIT offset, size 是最快的方案,但实际落地常因排序字段重复而漏数据或翻页错乱:
- 如果排序字段是
datetime类型(如created_at),大量记录时间相同,仅靠WHERE created_at > '2025-01-01'会跳过同时间戳的其他行 - 正确做法是复合排序:在
ORDER BY created_at DESC, id ASC的同时,索引建为(created_at, id),下一页参数传last_seen_created_at和last_seen_id两个值 - 首次查询必须加
LIMIT N+1(比如要 20 条,查 21 条),取第 21 条的(created_at, id)作为下一页游标,否则无法判断是否还有下一页
EXPLAIN 看不出问题?重点盯这 3 个字段
加了索引却没提速,往往是因为执行计划被误读。真正要盯的是:
-
type:必须是range或ref,如果是index,说明在全索引扫描,优化没起效 -
rows:数值应接近你想要的size(比如LIMIT 20时rows是 20~50),而不是offset + size(如 1000020) -
Extra:出现Using filesort或Using temporary就是排序没走索引;Using index表示覆盖索引生效,Using where; Using index更理想
一个容易被忽略的细节:MySQL 8.0+ 对 ORDER BY ... LIMIT 有优化,但仅限于单列主键或唯一索引排序。只要涉及 WHERE 过滤 + 复合排序,就必须靠人工建对索引,优化器不会自动补救。











