分页为什么越翻越慢:offset 陷阱、seek 分页与索引排序优化

目标:你能把"分页慢"讲成一个可解释的 IO 模型,并掌握可落地的改造:seek 分页、覆盖索引、延迟关联。

1. offset 分页的本质:扫描 + 丢弃

经典写法:

sql 复制代码
select *
from t
where user_id = ?
order by create_time desc
limit 100000, 20;

直觉:

  • MySQL 必须先找到前 100000 行(满足 where + order 的结果)
  • 然后丢弃,再返回 20 行

这意味着:

  • 页码越大,被丢弃的行越多
  • 扫描成本线性增长

1.1 一个可复现的最小例子:同一张表对比 offset 与 seek 的 EXPLAIN

准备表:

sql 复制代码
create table t_order (
  id bigint primary key,
  user_id bigint not null,
  create_time datetime not null,
  title varchar(64) not null,
  content varchar(2000) not null,
  key idx_user_time (user_id, create_time, id, title)
);

对比两条查询。

对照 1:offset 分页(越翻越慢)

sql 复制代码
explain
select *
from t_order
where user_id = 1
order by create_time desc, id desc
limit 10000, 20;

你要重点观察:

  • rows 是否显著变大
  • 是否发生大量回表(select *Extra 不会 Using index

对照 2:seek 分页(稳定)

sql 复制代码
explain
select id, title, create_time
from t_order
where user_id = 1
  and (create_time < ? or (create_time = ? and id < ?))
order by create_time desc, id desc
limit 20;

你要重点观察:

  • rows 不随页码线性增长
  • 更容易出现 Using index(覆盖)

2. 即使走索引,也会慢:因为"走的是长距离顺扫"

如果索引是 (user_id, create_time)

  • 能按顺序找到记录
  • 但仍需要向后移动 100000 步才能到达起点

如果还 select *

  • 会回表 100020 次(或接近)
  • 随机 IO/缓存失效进一步放大

3. seek 分页:把"跳过"变成"从游标继续"

思路:

  • 用上一页最后一条记录的排序键作为游标
  • 下一页从游标之后继续取

示例:

sql 复制代码
select *
from t
where user_id = ?
  and (create_time < ?)
order by create_time desc
limit 20;

如果存在同时间戳并发写入,建议加 tie-breaker:

sql 复制代码
where user_id=?
  and (create_time < ? or (create_time = ? and id < ?))
order by create_time desc, id desc
limit 20;

对应索引:

  • (user_id, create_time, id)

优势:

  • 不随页码变慢
  • 能稳定利用索引有序性

3.1 对照组:只用 create_time 做游标可能不稳定

如果你的数据里存在相同时间戳:

  • 错:只用 create_time < lastTime,可能出现漏数据/重复数据
  • 对:加 tie-breaker(create_time 相等时用 id

4. 覆盖索引 + 延迟关联:解决回表放大

4.1 列表页优先覆盖索引

如果列表只需要少量列:

  • 让索引覆盖返回列
  • Extra: Using index

4.2 必须返回全字段:用延迟关联压缩回表次数

sql 复制代码
select *
from t
where id in (
  select id
  from t
  where user_id=?
  order by create_time desc
  limit 20
);

目的:

  • 内层只拿 20 个 id
  • 外层只回表 20 次

对照点:延迟关联只解决"回表次数",不解决 offset 的"扫描丢弃"。

  • 页码很深时:优先 seek 分页
  • 页码不深但 select * 很重:延迟关联收益明显

5. 排序为什么会慢:filesort 与临时表

where 用的索引与 order by 不一致:

  • MySQL 可能先过滤再排序
  • 触发 Using filesort

优化:

  • 让联合索引同时满足 where + order
  • 让排序字段方向一致(desc/asc)

6. 线上排查 checklist

  • 是否大 offset:limit 100000, 20
  • EXPLAIN:
    • rows 是否随页码增长
    • Extra 是否 Using filesort/temporary
    • 是否 Using index(覆盖)
  • 是否 select * 导致回表放大

6.1 更流程化的排查顺序(从现象到动作)

  1. 确认现象
    • 是否"页码越大越慢"
  2. 用 EXPLAIN 看 3 个指标
    • rows 是否随 offset 增长
    • Extra 是否 Using filesort/temporary
    • 是否覆盖索引 Using index
  3. 按根因选方案
    • offset 导致的扫描丢弃:改 seek 分页
    • 回表放大:覆盖索引或延迟关联
    • 排序代价:调整联合索引让 where+order 同索引
相关推荐
05664610 小时前
RAG 向量检索:从“查字“到“查意“
数据库·人工智能·学习·oracle
bug嘛我经常写10 小时前
dbf文件UTF-8转GBK编码,以及两编码文件互转
数据库·python
知行合一。。。10 小时前
LangGraph--03--本地服务与 Studio 调试
数据库
你不是我我10 小时前
【AI 测评】PostgreSQL主从流复制实战:数据同步、状态验证与故障切换
数据库·postgresql
月落归舟10 小时前
Redis 三种消息队列实现方案
数据库·redis·list
小白说大模型10 小时前
【MYSQL】MYSQL学习的一大重点:索引(下)- B+树
b树·学习·mysql
2601_9498166810 小时前
mega fusion:从公开信息出发梳理外汇投教内容建设与投教内容
mysql
智购科技智能售货柜10 小时前
自动售货机商品识别YOLO模型训练实战:从6万张图片到98%识别率的完整复盘~YH
运维·服务器·数据库·人工智能·redis·物联网·yolo
ACP广源盛1392462567311 小时前
2026 PCIe互连芯片@ACP#国产替代格局解析:芯动科技领跑高端交换芯片赛道
大数据·网络·数据库·人工智能·分布式·嵌入式硬件
代码代码快快显灵11 小时前
MYSQL-DAY2
数据库·mysql