MySQL深分页超时是执行逻辑固有缺陷所致,必须扫描前offset+size行再丢弃;优化需用游标分页(WHERE id > ? ORDER BY id LIMIT 20)或延迟关联(子查询查id再JOIN),二者均依赖正确复合索引和唯一排序字段。

直接用 LIMIT offset, size 翻到几万页后,查询超时不是配置问题,是 MySQL 扫描逻辑本身决定的——它必须读完前 offset + size 行再丢弃,数据量越大,扫描越慢,超时几乎是必然结果。
为什么 LIMIT 1000000, 20 会超时?
MySQL 不会跳过前 100 万行,而是:先全量扫描 1000020 行(含排序),再丢弃前 100 万行,只返回最后 20 行。即使有索引:
- 如果
ORDER BY字段和WHERE条件不能共用一个联合索引,就会触发回表或全索引扫描 - 若查询
SELECT *,InnoDB 还要根据主键逐行回聚簇索引取完整数据,产生大量随机 I/O - 优化器可能因统计信息过期,错误选择主键索引而非时间/状态等筛选字段索引,导致扫描行数暴增
游标分页(WHERE id > ? ORDER BY id LIMIT 20)怎么写才不翻车?
这是目前最稳定、性能恒定的方案,但对使用方式有硬性约束:
- 必须有单调递增(或严格有序)的排序字段,推荐用自增
id或带毫秒精度的create_time - 不能支持“跳转到第 888 页”这种随机页码,只适合连续下一页(
next)场景 - 注意边界处理:上一页最后一条记录的
id是 999999,下一页必须写WHERE id > 999999,不能写>=,否则会重复 - 如果排序字段允许重复(比如多个记录同秒创建),需补上第二排序字段防错乱:
WHERE (create_time, id) > ('2026-06-18 10:00:00', 500000) ORDER BY create_time, id LIMIT 20
延迟关联(JOIN 子查询)为什么比 IN 好?
很多人想用 SELECT * FROM t WHERE id IN (SELECT id FROM t LIMIT 100000, 20),但 MySQL 会报错:子查询不能带 LIMIT。正确写法是:
SELECT t1.* FROM t1 INNER JOIN ( SELECT id FROM t1 ORDER BY id LIMIT 100000, 20 ) AS tmp ON t1.id = tmp.id;
关键点:
- 子查询只走覆盖索引(如主键索引),扫描快、内存占用小
- 外层
JOIN是等值匹配,走主键查找,只需读取 20 行完整数据 - 比
IN更可控:MySQL 对IN列表长度有限制,且优化器有时会将IN转为OR导致索引失效 - 如果排序字段不是主键(比如按
status, create_time),必须确保该字段组合有联合索引,否则子查询仍会慢
索引设计不当会让所有优化失效
再好的分页写法,遇上错误索引也白搭。常见坑:
- 只在
create_time上建单列索引,但查询既要WHERE create_time BETWEEN ? AND ?又要ORDER BY id—— 这时 MySQL 往往放弃该索引,改走主键索引全扫 - 正确做法是建联合覆盖索引:
CREATE INDEX idx_create_time_id ON t(create_time, id),让范围筛选和排序都在同一索引内完成 - 如果还要查
name、email等字段,且无法接受延迟关联,就把它们也加进索引变成覆盖索引:CREATE INDEX idx_ct_id_name_email ON t(create_time, id, name, email) - 建完索引别忘了更新统计信息:
ANALYZE TABLE t,否则优化器可能继续误判
真正卡住人的往往不是语法,而是业务是否允许放弃“跳页”、排序字段是否真能唯一锚定位置、以及有没有人记得去 ANALYZE TABLE。这几个点漏掉一个,前面所有优化都可能打折扣。


















