MySQL 深分页越来越慢?从 LIMIT OFFSET 改成游标分页

分页查询是后台系统中最常见的功能之一。

数据量较小时,通常会直接使用:

复制代码
SELECT *
FROM meeting_record
ORDER BY id DESC
LIMIT 20 OFFSET 0;

翻到第 1000 页后,SQL 可能变成:

复制代码
SELECT *
FROM meeting_record
ORDER BY id DESC
LIMIT 20 OFFSET 19980;

随着页码不断增加,查询速度往往会越来越慢。这类问题通常被称为"深分页"。

一、为什么 OFFSET 越大越慢?

LIMIT 20 OFFSET 19980 并不是让数据库直接从第 19981 条数据开始读取。

数据库通常需要:

复制代码
先找到前 20000 条数据
丢弃前 19980 条
最后返回 20 条

当 OFFSET 达到几十万甚至几百万时,大量扫描结果最终都会被丢弃。

例如:

复制代码
SELECT *
FROM meeting_record
ORDER BY id DESC
LIMIT 20 OFFSET 500000;

为了返回 20 条记录,数据库可能需要扫描并跳过前面的 50 万条数据。

如果查询还包含排序、回表或复杂过滤,性能下降会更加明显。

二、先确认索引是否有效

假设查询条件为:

复制代码
SELECT *
FROM meeting_record
WHERE user_id = 10001
ORDER BY id DESC
LIMIT 20 OFFSET 100000;

可以增加联合索引:

复制代码
CREATE INDEX idx_user_id
ON meeting_record(user_id, id);

然后通过 EXPLAIN 查看执行计划:

复制代码
EXPLAIN
SELECT *
FROM meeting_record
WHERE user_id = 10001
ORDER BY id DESC
LIMIT 20 OFFSET 100000;

重点关注:

复制代码
key:是否使用预期索引
rows:预计扫描多少行
Extra:是否出现 filesort

索引可以减少扫描范围,但无法完全消除大 OFFSET 带来的开销。

三、使用游标分页

如果业务不要求用户直接跳转到任意页,可以改成游标分页。

第一页:

复制代码
SELECT *
FROM meeting_record
WHERE user_id = 10001
ORDER BY id DESC
LIMIT 20;

假设最后一条记录的 ID 是 9520,下一页查询:

复制代码
SELECT *
FROM meeting_record
WHERE user_id = 10001
  AND id < 9520
ORDER BY id DESC
LIMIT 20;

前端只需要把上一页最后一条记录的 ID 传回来:

复制代码
{
  "lastId": 9520,
  "pageSize": 20
}

这种方式不需要数据库跳过大量无用记录,查询性能通常更加稳定。

Java 示例:

复制代码
public List<MeetingRecord> queryNextPage(
    Long userId,
    Long lastId,
    Integer pageSize
) {
    return meetingRecordMapper.queryNextPage(
        userId,
        lastId,
        pageSize
    );
}

对应 SQL:

复制代码
SELECT *
FROM meeting_record
WHERE user_id = #{userId}
  AND id < #{lastId}
ORDER BY id DESC
LIMIT #{pageSize};

四、游标分页有什么限制?

游标分页也不是万能方案。

它不适合:

复制代码
直接跳转到第 100 页
展示准确的总页数
数据排序字段频繁变化

它更适合:

复制代码
下拉加载
消息列表
操作日志
会议记录
订单流水
时间线

例如**同言翻译(Transync AI)**在生成实时翻译记录和 AI 会议总结后,如果用户需要浏览大量历史会议,记录列表就更适合使用游标分页,而不是不断增大的 OFFSET。

五、排序字段不唯一怎么办?

如果按创建时间排序:

复制代码
ORDER BY created_at DESC

可能有多条数据拥有相同时间。

只使用:

复制代码
WHERE created_at < ?

可能导致部分记录重复或遗漏。

更稳妥的方式是使用复合游标:

复制代码
SELECT *
FROM meeting_record
WHERE user_id = 10001
  AND (
      created_at < '2026-06-16 10:00:00'
      OR (
          created_at = '2026-06-16 10:00:00'
          AND id < 9520
      )
  )
ORDER BY created_at DESC, id DESC
LIMIT 20;

同时建立索引:

复制代码
CREATE INDEX idx_user_time_id
ON meeting_record(user_id, created_at, id);

这样即使创建时间相同,也可以通过 ID 保证稳定顺序。

六、必须跳页时怎么优化?

部分后台管理系统确实需要跳转页码。

可以先使用覆盖索引查出主键:

复制代码
SELECT id
FROM meeting_record
WHERE user_id = 10001
ORDER BY id DESC
LIMIT 20 OFFSET 100000;

再根据主键查询完整数据:

复制代码
SELECT *
FROM meeting_record
WHERE id IN (...);

也可以写成关联查询:

复制代码
SELECT m.*
FROM meeting_record m
JOIN (
    SELECT id
    FROM meeting_record
    WHERE user_id = 10001
    ORDER BY id DESC
    LIMIT 20 OFFSET 100000
) t ON m.id = t.id
ORDER BY m.id DESC;

这种方式仍然需要扫描较大的 OFFSET,但可以减少扫描阶段的回表数据量。

七、分页优化检查清单

遇到深分页问题时,可以依次检查:

复制代码
1. 查询条件是否建立合适索引
2. ORDER BY 是否可以使用索引
3. 是否真的需要跳转到任意页
4. 是否可以改成游标分页
5. 排序字段是否唯一
6. 是否需要使用复合游标
7. 是否可以先查主键再查询完整数据
8. 是否需要限制最大可翻页数

总结

MySQL 深分页变慢的主要原因,是数据库需要扫描并丢弃大量数据。

如果业务是消息流、日志、会议记录或订单流水,优先考虑:

复制代码
索引排序 + 游标分页

如果必须支持任意页跳转,可以尝试:

复制代码
覆盖索引 + 主键回表

分页优化的关键不是单纯修改 LIMIT,而是根据业务交互方式选择合适的分页模型。

相关推荐
Lethehong5 小时前
Oracle 加个字段,目标库就报错?KFS 用“先拆后组“打通异构 DDL
数据库·oracle
Web极客码6 小时前
锁定真正的磁盘卡顿:我用 OpenCode 配合 eBPF 诊断 NVMe 队列深度延迟与 I/O 写放大
数据库·oracle
cspttty6 小时前
2027用户研究求职准备:应用统计学如何补齐研究设计与业务表达
大数据·数据库
roman_日积跬步-终至千里7 小时前
【数据Agent论文·查询与语义|05】Spider:为什么换一个数据库,模型就不会查了?
数据库·数据分析
猫咪很可爱8 小时前
Oracle RAC 启动失败排查:私网通信与内核参数优化实战
运维·数据库·oracle
XLYcmy8 小时前
《数字经济赋能城市低碳治理的路径研究检索报告》审阅与修改指导意见——基于完整检索报告体例的完整性诊断、学术规范核查与方法论纠偏 上
数据库·ai·chatgpt·文心一言·检索·知网·豆包
泡海椒8 小时前
JQuick-SQL 快速开始:注册内存表并执行查询
数据库·windows·sql
ss2739 小时前
PG实战笔记 | 09 PostgreSQL psql客户端基本操作
数据库·笔记·postgresql
电商API_180079052479 小时前
反向海淘多币种结算:汇率该在哪个时点被冻结
java·开发语言·数据库·跨境电商·1688 api
搞AI的金鱼9 小时前
skill agent workflow的区别
数据库