mysql如何使用索引优化分页查询_mysql分页性能分析

来源:这里教程网 时间:2026-02-28 20:52:59 作者:

为什么 OFFSET 越大,LIMIT 查询越慢

MySQL 的

OFFSET
不是跳过已扫描的行,而是真实地扫描并丢弃前 N 行。比如
SELECT * FROM orders ORDER BY id LIMIT 10000, 20
,MySQL 会先按
id
排序,再逐行读取前 10020 行,只返回后 20 行——前 10000 行全白读了,还占 I/O 和 CPU。

如果没走索引或排序字段无索引,还会触发

Using filesort
,性能雪上加霜。

ORDER BY 字段必须有索引,且和 WHERE 条件能共用(最左前缀原则) 避免
SELECT *
,只查必要字段,减少回表开销
不要用
OFFSET
做“深度翻页”,10 万行之后基本不可控

用游标分页(Cursor-based Pagination)替代 OFFSET

核心思路:不依赖行号,改用上一页最后一条记录的排序键值作为查询起点。例如按

created_at DESC, id DESC
分页,第二页就从上一页末尾的
(created_at, id)
值继续查:

SELECT * FROM posts 
WHERE (created_at, id) < ('2024-05-01 10:20:30', 12345)
ORDER BY created_at DESC, id DESC 
LIMIT 20;

这个查询能命中联合索引

(created_at, id)
,全程走索引范围扫描,不依赖偏移量。

必须保证排序字段组合唯一(或加主键兜底),否则可能漏数据或重复 不能跳页(如直接跳到第 100 页),但对“下一页”场景极友好 前端需保存上一页末尾的游标值,而不是页码

覆盖索引 + 主键关联优化传统 LIMIT

当必须用页码(比如后台管理列表),可先用覆盖索引快速定位主键,再回表取数据:

SELECT p.* FROM posts p
INNER JOIN (
  SELECT id FROM posts 
  WHERE status = 1 
  ORDER BY created_at DESC, id DESC 
  LIMIT 10000, 20
) AS tmp ON p.id = tmp.id;

子查询只查

id
,若
status
和排序字段都在同一索引里(如
(status, created_at, id)
),就能完全走索引,不回表;外层再用主键关联取完整数据,大幅减少扫描量。

索引要包含 WHERE 字段 + ORDER BY 字段 + 主键(用于覆盖) 子查询的
LIMIT
仍存在深度偏移问题,但只作用于轻量级主键列,代价小很多
注意 MySQL 8.0+ 对这种写法优化更好,5.7 下需确认执行计划是否真用了索引

哪些索引设计会让分页失效

常见“假索引”陷阱:

INDEX(created_at)
单独存在,但查询带
WHERE user_id = 123
→ 无法同时满足过滤与排序,要么走
user_id
索引然后
filesort
,要么走
created_at
索引但全表扫
user_id
INDEX(user_id, created_at)
有,但排序是
ORDER BY created_at DESC, id DESC
id
不在索引中,仍需回表+额外排序
使用函数或表达式排序,如
ORDER BY DATE(created_at)
→ 索引失效,必然
filesort

真正有效的索引得匹配整个查询模式:WHERE 条件最左前缀 + ORDER BY 字段顺序 + 包含主键或所需查询列(覆盖)。

深度分页不是加个索引就能解决的事,关键在把“跳过 N 行”的思维,换成“从某条记录往后取”。游标分页看着麻烦,却是唯一能线性扩展的方式。

相关推荐