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
JOIN (SELECT id FROM table_1 WHERE is_deleted='N' ORDER BY id LIMIT 100000,20) b
ON a.id = b.id;

前提:子查询的 WHERE+ORDER BY 能被某个索引覆盖(本例 is_deleted+id 或 id 排序)。适用:仍需跳页(用户直接点第 5000 页)的场景。

方案二:游标分页(keyset / seek 法)

把 offset 转化为”上一页最后一条的键值”条件:

-- 第一页
SELECT * FROM table_1 WHERE is_deleted='N' ORDER BY id LIMIT 20;
-- 下一页(last_id = 上页最大 id)
SELECT * FROM table_1 WHERE is_deleted='N' AND id > :last_id ORDER BY id LIMIT 20;

每页都只扫 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 就不再是问题。