mysql深度分页

概念

当使用 limit 查询偏移量过大时(limit 通常用来分页),导致回表次数变多、走全表不利用索引,然后导致查询性能变低。

如:SELECT * FROM t_order LIMIT 1000000, 10

会扫描并取 1000000+10 行,然后放弃前 1000000 行,返回 10 行。 高并发下容易造成 CPU、IO、内存暴涨,甚至 OOM。 不过,即使偏移量很大,如果查询中使用了覆盖索引,MySQL 仍然可能会使用索引,避免回表操作。

一句话总结:“深分页不用 OFFSET,用条件(where)定位起点” 是所有优化手段的核心,从而避免大偏移量带来的全表扫描和大量回表。

游标/范围分页(推荐)

原理:记住上一页最后一条记录的主键(或排序列),作为下一页的起始条件。
示例

1
2
3
4
5
-- 上一页最后一条 id = 100000
SELECT * FROM t_order
WHERE id > 100000        -- 无 OFFSET
ORDER BY id
LIMIT 10;
  • 优点:直接跳到 100000,性能稳定,不随页码增大而衰减;千万级数据仍可毫秒级返回。
  • 缺点:只能顺序翻页,不能随机跳页;需要排序列唯一且连续。

  延迟关联(JOIN 子查询)

原理先用覆盖索引在子查询里拿到主键,再回表取完整行,减少大 OFFSET 的回表量。
示例

1
2
3
4
5
6
7
SELECT t1.*
FROM t_order t1
JOIN (SELECT id
      FROM t_order
      WHERE create_time > '2023-01-01'
      ORDER BY create_time
      LIMIT 4500000, 10) t2 ON t1.id = t2.id;
  • 优点:比直接 LIMIT 4500000,10 (该方法先数 4500000 条数据,然后丢掉,)少读 450w 行数据。
  • 注意:子查询里的列必须被索引覆盖,否则仍可能全表扫描。

覆盖索引 + 子查询定位起点

原理:先用覆盖索引查出第 N 条记录的排序列值,再用该值作为条件取后 10 条。
示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
-- 1. 查出第 4500000 行的 create_time
SELECT create_time
FROM t_order
ORDER BY create_time
LIMIT 4500000, 1;

-- 2. 用该 create_time 作为起点
SELECT *
FROM t_order
WHERE create_time >= <上一步的值>
ORDER BY create_time
LIMIT 10;
  • 优点:避免大 OFFSET;支持非主键排序。
  • 注意:排序列必须有索引,否则第 1 步也会很慢。

预计算或缓存

  • 静态数据可提前算好页码结果,放入 Redis 或 ES,彻底避开 MySQL。
  • 动态数据可用异步任务把每页首条主键写入缓存,查询时直接 > 条件。
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计