为什么 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就直接拖垮数据库。压测时务必用接近生产规模的数据集跑分页查询。
