limit 20000 加载很慢怎么解决
MySQL 的性能低是因为数据库要去扫描 N + M 条记录,然后又要放弃之前 N 条记录,开销很大
解决思路:
- 前端加缓存,或者其他方式,减少落到库的查询操作,例如某些系统中数据在搜索引擎中有备份的,可以用 es 等进行搜索
- 使用延迟关联,即先通用 limit 得到需要数据的索引字段,然后再通过原表和索引字段关联获得需要数据
select a.* from a,(select id from table_1 where is_deleted='N' limit 100000,20) b where a.id = b.id |
- 从业务上实现,不分页如此多,例如只能分页前 100 页,后面的不允许再查了
- 不使用 limit N,M, 而是使用 limit N,即将 offset 转化为 where 条件。
慢的根因(扫 N+M 扔 N)与四条思路都对,但还缺三块:为什么”扔 N 条”这么贵(回表视角)、游标法(keyset)的完整写法与适用边界、以及各方案的组合策略。本文补全成深分页优化手册。
根因深挖:offset 的两重代价
LIMIT 100000, 20 的执行:沿索引(或全表)扫过 100020 行,每行若需回表则 10 万次随机 IO 全浪费在前 10 万行,最终只返回 20 行。代价 = offset 越大,浪费的扫描+回表越多。两个放大因子:
- 回表:二级索引扫描时每行都要回聚簇索引取列 → 10 万次随机 IO
- 排序:带 ORDER BY 非索引列时还要 filesort 全量 offset+limit 行
方案一:延迟关联(原文 SQL 的原理)
子查询只在覆盖索引上扫 offset+limit 行(不回表,索引叶节点即 id),拿到 20 个 id 后再回表取整行——回表次数从 10 万降到 20:
SELECT a.* FROM table_1 a |
前提:子查询的 WHERE+ORDER BY 能被某个索引覆盖(本例 is_deleted+id 或 id 排序)。适用:仍需跳页(用户直接点第 5000 页)的场景。
方案二:游标分页(keyset / seek 法)
把 offset 转化为”上一页最后一条的键值”条件:
-- 第一页 |
每页都只扫 20 行(索引范围扫起点直达),复杂度与页深无关——深分页的终极解。边界:
- 只支持”上一页/下一页”式连续翻页,不支持随机跳页
- ORDER BY 键必须唯一且有序(常用主键或唯一索引列),多列排序需复合条件
(a,b) > (:la,:lb)展开 - 前端交互需保留游标(无限滚动/加载更多天然契合)
方案三:业务约束(原文第三条的展开)
- 限制最大页深(只允许前 100 页):搜索引擎(Google 也只给约几十页)的通用做法
- 改”页码分页”为”游标分页”交互(无限滚动)
- 计数优化:
COUNT(*)大表也慢 → 估算(explain 行数/信息_schema统计)或缓存计数或”是否有下一页”(limit 21 看有无第 21 条)代替精确总数
方案四:异构存储(原文第一条的展开)
- 复杂检索+深分页 → ES/OpenSearch:search_after 游标、或 accept 近似总数;ES 的 depth 限制(max_result_window)同样存在,深翻用 scroll/search_after
- 热点页缓存:前 N 页结果进 Redis/CDN,深页流量本就稀少(帕累托),缓存命中率极高
- 归档分离:历史数据迁冷库,热表行数降下来 offset 自然变小
组合策略与决策表
| 场景 | 方案 |
|---|---|
| 无限滚动/下一页 | 游标分页(首选) |
| 必须随机跳页 + 中深页 | 延迟关联 |
| 必须随机跳页 + 超深页 | 业务限深 + 延迟关联 |
| 复杂条件检索分页 | ES search_after |
| 热点前几页 | 缓存 |
| 总数展示 | 估算/缓存/“有无下一页” |
验证手段
EXPLAIN ANALYZE 看实际扫描行数(rows examined)与回表;慢查询日志 Rows_examined 是 offset 浪费的直接证据——优化前后对比该指标应从”offset+limit”降到”limit 量级”。
深分页优化的本质:不让数据库做”扫过又扔掉”的无用功——游标法让扫描起点直达、延迟关联让无用扫描不回表、业务与异构方案让深页请求根本不落到主库。四招按场景组合,limit 20000 就不再是问题。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 非鱼小站!
评论
WalineDisqus











