MySQL大数据量分页查询优化方法
本质上,LIMIT offset, size 在深分页场景下之所以越来越慢,关键原因在于:MySQL 通常需要先完成整体排序,再跳过前面的 offset 行,这个过程成本很高。更合适的优化方式,一般是使用延迟关联——先查询 ID,再通过 JOIN 获取完整数据;或者改用书签分页,也就是结合 WHER
本质上,LIMIT offset, size 在深分页场景下之所以越来越慢,关键原因在于:MySQL 通常需要先完成整体排序,再跳过前面的 offset 行,这个过程成本很高。更合适的优化方式,一般是使用延迟关联——先查询 ID,再通过 JOIN 获取完整数据;或者改用书签分页,也就是结合 WHERE 条件和唯一排序字段向后翻页。同时还有一个不能忽视的前提:索引设计必须与 WHERE、ORDER BY 的字段顺序保持一致,性能才能稳定。

如果直接使用 LIMIT offset, size 查询千万级数据,翻到第 100 页时通常就会出现明显变慢,到了第 1000 页基本难以接受。这并不是代码质量问题,而是 MySQL 的执行机制决定的。要解决这个问题,必须调整分页思路,仅靠增大 timeout 或单纯增加索引,并不能从根本上改善深分页性能。
为什么 LIMIT 1000000, 20 会非常慢
MySQL 并不会“直接跳到第 1000001 行再读取 20 条”,实际执行过程通常是:先根据 ORDER BY 字段对所有符合条件的数据完成排序(可能达到数十万甚至上百万行),然后从头开始计数到 1000020 条,丢弃前面的 1000000 条,最后仅返回后面的 20 条。
- 排序本身就可能消耗大量内存或磁盘临时表,
EXPLAIN结果中Extra列出现Using filesort,通常就是一个明显信号。 - 如果
ORDER BY字段没有索引,或者在条件中使用了函数,例如WHERE DATE(created_at) = '2026-01-01',就很容易退化为全表扫描。 SELECT *会进一步放大问题。即使最终只返回 20 条记录,也可能需要读取每一行的全部字段、完成磁盘访问、网络传输以及客户端解析。
延迟关联方案(JOIN 子查询)怎样写才有效
核心思路是“先查 ID,再查详情”,让内层查询尽量只走索引,外层再按主键精准回表。不过如果写法不当,优化效果很容易失效。
- 子查询应当只查询
id(或者排序字段加主键),并且ORDER BY字段必须有索引,否则子查询本身仍然会出现Using filesort。 - 外层
JOIN关联字段必须是主键或唯一索引,否则执行计划可能退化为ALL扫描。 - 示例写法:
SELECT t.* FROM orders t JOIN (SELECT id FROM orders WHERE status = 1 ORDER BY created_at DESC LIMIT 1000000, 20) tmp ON t.id = tmp.id;
- 不推荐的写法是子查询直接带
*或查询非索引字段,例如:SELECT * FROM orders WHERE status = 1 ORDER BY created_at DESC LIMIT 1000000, 20,这种写法与原始深分页方式本质上没有区别。
书签分页(WHERE id > )的边界问题
书签分页更适合后台列表这种持续点击“下一页”的场景,但对跳页、排序变化以及数据实时增删都比较敏感。
- 必须保证排序字段组合可以唯一确定一条记录,否则就可能出现重复或漏数据。比如按
created_at DESC, id DESC排序,若上一页最后一条记录是(created_at='2026-08-10 12:00:00', id=9999),下一页条件应写为:WHERE created_at <= '2026-08-10 12:00:00' AND (created_at < '2026-08-10 12:00:00' OR id < 9999) ORDER BY created_at DESC, id DESC LIMIT 20
- 如果业务允许,优先使用自增主键
id排序通常更稳定,例如:WHERE id > 9999 ORDER BY id LIMIT 20。 - 不要只写成
WHERE created_at >这种单条件形式,因为同一秒内插入多条数据时,几乎一定会出现漏数据问题。
实际优化中最容易被忽略的点
分页优化并不是简单替换一条 SQL 就能完成。很多线上性能问题,往往卡在更基础的执行层面。
- 如果
EXPLAIN中看到type为ALL或index,通常说明WHERE条件并没有有效利用索引。在这种情况下,分页优化很难真正发挥作用,应先解决索引命中问题。 - 复合索引的字段顺序必须和
WHERE + ORDER BY的使用顺序匹配。例如WHERE status = 1 ORDER BY created_at DESC,更合适的索引是(status, created_at),如果顺序反过来,效果通常不理想。 - MySQL 5.7 默认的
sort_buffer_size往往较小,大结果集排序时容易落盘。可以通过SHOW VARIABLES LIKE 'sort_buffer_size';查看当前配置,必要时适当调高,但也不宜设置过大,以免占用过多内存。
