目录
1.问题
MySQL 的深度分页(如 LIMIT 1000000, 20)会产生以下严重问题:
- 性能问题 :
- 要扫描前 N 条记录:即使只取 20 条,MySQL 也必须读取前 1000000 条记录并丢弃
- 随着页码增大性能急剧下降:第 1 页很快,第 10 万页很慢
- 内存压力 :
- 大量非目标数据进入内存:扫描过程中加载大量索引页和数据页
- 热点数据被挤出:频繁访问的数据页可能被这些扫描数据替换出缓冲池
- 内存命中率下降:导致其他查询性能也受影响
- I/O 开销 :
- 回表操作:每条索引记录都需要随机读取数据页
- 大量随机读取:前 100 万条记录的索引扫描虽然顺序,但回表是随机 I/O
- 网络和连接问题 :
- 大量无用数据传输:虽然最终只返回 20 条,但内部处理过程中有大量数据传输
- 网络延迟累积:特别是在分布式环境中,内部数据传输消耗大量网络资源
- 执行时间过长:可能超过
wait_timeout或interactive_timeout设置 - 连接池耗尽:慢查询占用连接,影响其他业务
2.优化方案
2.1.游标分页
(1)游标分页是一种基于指针 (Cursor) 的分页方式,它通过记住上一页最后一条数据的位置,来获取下一页数据。游标分页对索引的核心限制是:游标字段必须是排序字段的一部分,且索引设计必须同时满足过滤条件和排序条件。其工作原理如下:
- 不依赖页码,而是依赖上一次查询的最后一条记录
- 使用 WHERE 排序字段 > 上一页最大值的方式获取下一页
- 类似"加载更多"而不是"跳转到第 N 页"
sql
-- 下一页
SELECT * FROM orders
WHERE id > last_id
ORDER BY id
LIMIT 20;
同理,查询上一页的 SQL 语句如下:
sql
-- 上一页
SELECT * FROM orders
WHERE id < first_id
ORDER BY id DESC -- 注意排序方向相反
LIMIT 20;
-- 然后在应用层反转数组
(2)一般来说,客户端的请求参数与服务器端的响应格式可设计如下:
java
请求参数:
- limit: 每页数量
- cursor: 游标(可选,第一页不传)
- direction: 'next' 或 'prev'(可选,默认next)
响应格式:
{
"data": [...],
"prevCursor": "xxx", // 用于上一页
"nextCursor": "yyy", // 用于下一页
"hasPrev": true,
"hasMore": true
}
2.2.使用子查询进行延迟关联
子查询只覆盖索引扫描,不需要回表,大大减少了 I/O 开销。具体对比如下:
sql
-- 直接查询
SELECT * FROM orders ORDER BY create_time LIMIT 100000, 10;
直接查询的执行流程:
- 扫描二级索引 (create_time) 找到前 100010 条记录的主键 ID
- 对每个 ID 进行回表(100010 次回表)
- 获取完整数据行
- 排序后返回最后 10 条
- 内存负担:100010 条完整数据行都加载到内存
sql
-- 优化后:先查主键,再关联
SELECT o.*
FROM orders o
INNER JOIN (
SELECT id
FROM orders
ORDER BY create_time
LIMIT 100000, 10
) tmp ON o.id = tmp.id;
执行流程:
- 子查询只扫描二级索引 (create_time) 获取 100010 个 ID(无需回表)
- 子查询的 SELECT id 可以使用覆盖索引,所有数据都在索引页中
- 子查询返回 10 个 ID (LIMIT 100000, 10)
-主查询用这 10 个 ID进行精准回表(仅 10 次回表) - 内存负担:只加载 10 条完整数据行
2.3.覆盖索引
(1)设置覆盖索引的核心思路就是把查询需要用到的字段都放到同一个索引中去,这样查询所需的全部数据都在索引中,从而不需要进行回表操作。
sql
-- 创建覆盖索引
ALTER TABLE orders ADD INDEX idx_cover_page (id, order_no, amount);
-- 使用了覆盖索引的 SQL 语句
SELECT id, order_no, amount FROM orders ORDER BY id LIMIT 1000000, 20;
(2)使用 EXPLAIN 验证是否生效
sql
EXPLAIN SELECT id, order_no, amount FROM orders ORDER BY id LIMIT 1000000, 20;
关键看 Extra 字段:
Using index:表示使用了覆盖索引,查询完全在索引中完成,无需回表Using where、Using filesort:表示没有用到索引或需要额外排序
(3)注意事项:
- 只查询部分字段:不能是 SELECT *,必须明确指定字段列表
- 字段不要太多:索引字段过多会导致索引臃肿,影响写入性能
- B-Tree 索引才能覆盖:哈希索引、全文索引不支持覆盖索引
有关覆盖索引的相关知识可以查看 MySQL 高级篇知识点------索引优化与查询优化这篇文章中的第 8 节。
2.4.限制最大页数
在产品层面限制用户只能查看前 N 页,比如最多 100 页,超过时提示用户缩小搜索范围。