LIMIT 越大越慢是因为MySQL需扫描并跳过前offset+size行,导致I/O和CPU开销剧增;推荐游标分页或延迟关联优化,避免全表扫描与文件排序。

为什么 LIMIT 越大查询越慢?
不是 LIMIT 本身慢,而是 MySQL 在执行 LIMIT 10000, 20 这类语句时,必须先扫描前 10020 行,再丢弃前 10000 行——数据量越大,跳过的行越多,I/O 和 CPU 开销越明显。
尤其当排序字段无索引、或 ORDER BY 和 LIMIT 组合使用但未命中覆盖索引时,性能会断崖式下降。
- 全表扫描 + 文件排序(
Using filesort)是常见瓶颈 -
EXPLAIN中rows值远大于实际返回行数,说明“扫得多、取得少” - 主键自增且按主键分页时,
LIMIT性能尚可;但按时间、状态等非连续字段分页时,问题立刻暴露
用游标分页替代 LIMIT offset, size
把“第 N 页”这种逻辑,换成“从上次最后一条记录之后继续取”,彻底避开 offset 跳过成本。
前提是:分页字段有唯一性、有索引、且支持比较操作(如 id > ? 或 created_at > ? AND id > ?)。
- ✅ 正确示例(假设
id是主键):SELECT * FROM orders WHERE id > 12345 ORDER BY id LIMIT 20 - ⚠️ 注意多条件场景:若按
status, created_at排序,需组合条件避免重复或遗漏,例如:WHERE (status, created_at, id) > ('paid', '2024-01-01', 999) ORDER BY status, created_at, id LIMIT 20 - ❌ 不要依赖
OFFSET做“上一页/下一页”切换——游标分页天然不支持随机跳页,这是设计取舍
强制走覆盖索引 + 延迟关联优化大偏移查询
当必须支持跳页(比如后台管理查第 200 页),又无法改用游标时,可用“先查主键,再回表”减少传输和排序开销。
核心思路:让 ORDER BY ... LIMIT 只在索引列上跑,避免读取整行数据。
- 建复合索引匹配查询条件与排序字段,例如:
ALTER TABLE users ADD INDEX idx_status_ctime_id (status, created_at, id); - 改写查询为两层:
SELECT u.* FROM users u INNER JOIN (SELECT id FROM users WHERE status = 'active' ORDER BY created_at DESC, id DESC LIMIT 10000, 20) t ON u.id = t.id; - 外层
JOIN的作用是只回表加载真正需要的 20 行,而非扫描 10020 行全字段 - 注意:MySQL 8.0+ 对这类子查询优化更好;5.7 及以前可能仍需加
STRAIGHT_JOIN提示
SQL_CALC_FOUND_ROWS 已废弃,别再用它统计总页数
MySQL 8.0 已移除 SQL_CALC_FOUND_ROWS,且它本身就有严重性能问题:即使只取 20 条,也要完整扫描符合条件的全部行来算总数。
真实业务中,“总共有多少页”往往只是个模糊参考,没必要精确。
- ✅ 更轻量方案:用
SELECT COUNT(*)单独查总数,但加缓存(如 Redis)或异步更新 - ✅ 或估算:对大表用
SHOW TABLE STATUS的Rows字段(InnoDB 是估算值,误差可接受) - ✅ 前端显示“下一页”即可,不显示“共 XX 页”——很多产品已这么做
- ❌ 避免在分页接口里同时返回
data和精确total,尤其当WHERE条件复杂时,两次查询代价翻倍
游标分页不是银弹——它要求前端保存上一页末尾的排序字段值,且无法跳转任意页;而延迟关联需要仔细设计索引和 SQL 结构。最容易被忽略的是:开发常在本地小数据量验证分页逻辑,上线后数据量增长十倍,LIMIT 50000, 20 就直接拖垮数据库。压测时务必用接近生产规模的数据集跑分页查询。











