MySQL 深分页优化:LIMIT 为什么越来越慢

在后台管理、订单查询、文章列表等业务中,分页几乎是最常见的 SQL 场景之一。

最常见的写法是:

SELECT id, title, created_at
FROM articles
ORDER BY id DESC
LIMIT 20 OFFSET 1000000;

刚开始数据量比较小时,这种分页方式没有明显问题。但随着数据量增长,翻到后面的页码后,查询速度可能明显下降。

很多人会简单地认为:

LIMIT OFFSET 会越来越慢,因为数据库扫描了很多数据。

这个说法没错,但还不够准确。真正需要理解的是:数据库为了找到第 1000001 条记录,通常仍然需要定位并跳过前面的 1000000 条符合条件的记录。

因此,深分页优化的核心并不是简单地给 LIMIT 加索引,而是要改变分页数据的定位方式。

1. 什么是深分页

假设 articles 表有 1000 万条数据:

SELECT id, title, created_at
FROM articles
ORDER BY id DESC
LIMIT 20 OFFSET 0;

查询第一页:

跳过 0 条
读取 20 条

查询第 100 页:

LIMIT 20 OFFSET 1980;

需要跳过约 1980 条记录。

而查询第 50000 页:

LIMIT 20 OFFSET 999980;

就需要跳过接近 100 万条记录。

如果继续向后翻:

LIMIT 20 OFFSET 9999980;

就需要处理接近 1000 万条记录。

因此:

OFFSET 越大
   ↓
需要跳过的数据越多
   ↓
查询成本增加
   ↓
深分页越来越慢

这里需要注意,OFFSET 并不是简单地让 MySQL “读取所有数据后再返回 20 条”。实际执行方式还取决于 WHERE 条件、索引、排序方式以及执行计划。

但对于典型的:

ORDER BY id DESC
LIMIT 20 OFFSET N

随着 N 增大,需要定位并跳过越来越多记录是深分页的主要问题。

2. 为什么有索引仍然会慢

很多开发者遇到深分页时,第一个反应是:

“是不是没有索引?”

假设已经存在主键:

PRIMARY KEY (id)

并且执行:

SELECT id, title
FROM articles
ORDER BY id DESC
LIMIT 20 OFFSET 1000000;

MySQL 可以利用主键索引按照 id 倒序访问。

但是:

OFFSET 1000000

并不会让 B+Tree 直接跳到“第 1000001 条逻辑记录”。

索引能够帮助 MySQL 快速找到数据,并按照索引顺序访问,但 OFFSET 本身仍然意味着需要跳过前面的结果。

可以简单理解为:

B+Tree 索引
      ↓
按照 id DESC 顺序访问
      ↓
第 1 条
第 2 条
第 3 条
...
第 1000000 条  ← 跳过
第 1000001 条  ← 开始返回

所以:

有索引可以降低分页成本,但不能从根本上消除 OFFSET 带来的深分页问题。

如果 SQL 还包含其他 WHERE 条件、非覆盖索引或者复杂排序,实际成本可能更高。

3. 第一种方案:普通 LIMIT 分页

普通分页并不是不能用。

例如:

SELECT id, title, created_at
FROM articles
ORDER BY id DESC
LIMIT 20 OFFSET 100;

对于后台管理系统来说,这种方式通常完全够用。

如果数据量只有几十万,用户最多查看几十页,没必要为了“理论上的性能问题”把分页系统设计得很复杂。

适合:

后台管理系统
运营管理页面
数据量较小的列表
用户不会翻很深的页面
需要直接跳转到指定页

因此深分页优化并不是:

“所有 OFFSET 都应该被禁止。”

而应该根据实际数据规模和访问场景决定。

4. 第二种方案:延迟关联

如果必须保留传统页码分页,可以考虑延迟关联(Deferred Join)。

假设文章表:

CREATE TABLE articles (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    title VARCHAR(200) NOT NULL,
    content LONGTEXT NOT NULL,
    created_at DATETIME NOT NULL,

    PRIMARY KEY (id)
);

原始 SQL:

SELECT id, title, content, created_at
FROM articles
ORDER BY id DESC
LIMIT 20 OFFSET 1000000;

如果表比较大,并且需要读取大量非索引字段,那么可以先利用索引找到需要的主键:

SELECT id
FROM articles
ORDER BY id DESC
LIMIT 20 OFFSET 1000000;

然后再根据这些 ID 查询完整记录:

SELECT id, title, content, created_at
FROM articles
WHERE id IN (...);

也可以写成:

SELECT a.id, a.title, a.content, a.created_at
FROM articles a
JOIN (
    SELECT id
    FROM articles
    ORDER BY id DESC
    LIMIT 20 OFFSET 1000000
) t ON t.id = a.id
ORDER BY a.id DESC;

它的核心思想是:

先通过索引定位 ID
        ↓
只取需要的 20 个 ID
        ↓
再回表获取完整数据

这种方式主要减少深分页过程中不必要的回表成本。

但要注意:

延迟关联并没有消除 OFFSET 本身的扫描成本。

如果已经需要跳过 100 万条索引记录,它仍然需要处理这部分索引记录。

因此延迟关联更适合:

必须保留页码分页
SELECT 字段较多
数据量较大
存在明显回表成本

它不是深分页的终极解决方案。

5. 第三种方案:Keyset Pagination

对于数据量较大的列表,通常更推荐 Keyset Pagination,也叫:

Cursor Pagination
Seek Pagination
游标分页

假设按照 id DESC 排序。

第一页:

SELECT id, title, created_at
FROM articles
ORDER BY id DESC
LIMIT 20;

假设最后一条记录:

id = 999981

下一页不再使用:

LIMIT 20 OFFSET 20;

而是:

SELECT id, title, created_at
FROM articles
WHERE id < 999981
ORDER BY id DESC
LIMIT 20;

继续下一页:

WHERE id < 999961

这样数据库就可以利用主键索引直接定位到:

id < 999981

的位置,然后继续向后读取 20 条。

查询方式变成:

第一页
    ↓
记录最后一个 ID = 999981
    ↓
下一页 WHERE id < 999981
    ↓
记录最后一个 ID = 999961
    ↓
下一页 WHERE id < 999961

而不是:

第 1 页
    ↓
第 2 页
    ↓
第 3 页
    ↓
...
第 50000 页

这就是 Keyset Pagination 的核心优势:

分页位置由索引中的“值”确定,而不是由 OFFSET 指定需要跳过多少条记录。

6. Keyset Pagination 的正确设计

实际项目中不能简单地认为:

WHERE id < ?

永远够用。

因为业务通常需要按照:

ORDER BY created_at DESC

排序。

如果 created_at 存在大量相同值,就需要增加一个唯一字段作为稳定排序条件。

例如:

SELECT id, title, created_at
FROM articles
WHERE
    created_at < ?
    OR (
        created_at = ?
        AND id < ?
    )
ORDER BY created_at DESC, id DESC
LIMIT 20;

对应索引:

KEY idx_created_id (created_at, id)

假设上一页最后一条:

created_at = '2026-09-09 10:00:00'
id = 1000

下一页:

SELECT id, title, created_at
FROM articles
WHERE
    created_at < '2026-09-09 10:00:00'
    OR (
        created_at = '2026-09-09 10:00:00'
        AND id < 1000
    )
ORDER BY created_at DESC, id DESC
LIMIT 20;

这里的 id 就是稳定排序的 tie-breaker。

因此,Keyset Pagination 通常要求:

排序字段具有较好的索引支持
+
排序顺序稳定
+
最好包含唯一字段作为最终排序条件

7. Keyset Pagination 的局限

Keyset Pagination 并不是所有场景都适合。

最大的区别是:

OFFSET Pagination

可以:

第 1 页
第 100 页
第 500 页
第 10000 页

直接跳转。

而 Keyset Pagination 更适合:

下一页
下一页
下一页

这种连续浏览模式。

例如:

新闻流
商品流
订单列表
消息列表
时间线
文章列表

非常适合 Keyset Pagination。

但如果业务要求:

输入页码 → 直接跳到第 5000 页

那么 Keyset Pagination 就不太适合单独使用。

所以实际项目中经常采用:

后台管理系统
    → OFFSET Pagination

用户端无限滚动列表
    → Keyset Pagination

大数据量连续列表
    → Keyset Pagination

必须跳页 + 数据量很大
    → 根据业务设计混合方案

8. 复杂查询中的深分页

真实业务通常不会只有:

ORDER BY id DESC

例如订单列表:

SELECT id, order_no, amount, status, created_at
FROM orders
WHERE user_id = ?
  AND status = ?
ORDER BY created_at DESC, id DESC
LIMIT 20 OFFSET 100000;

如果采用 Keyset Pagination,可以转换成:

SELECT id, order_no, amount, status, created_at
FROM orders
WHERE user_id = ?
  AND status = ?
  AND (
      created_at < ?
      OR (
          created_at = ?
          AND id < ?
      )
  )
ORDER BY created_at DESC, id DESC
LIMIT 20;

同时根据实际查询设计索引,例如:

KEY idx_user_status_created_id (
    user_id,
    status,
    created_at,
    id
)

但联合索引是否应该采用这个顺序,不能仅凭经验判断,还需要结合:

WHERE 条件
ORDER BY
数据分布
选择性
实际 SQL
EXPLAIN

进行验证。

因此深分页优化实际上和索引设计、执行计划分析密切相关。

9. 实战中的选择

可以把几种方案简单归纳为:

方案优点缺点适合场景
LIMIT OFFSET简单、支持跳页深分页性能下降普通后台列表
延迟关联减少深分页回表成本OFFSET 成本仍然存在大表传统分页
Keyset Pagination深分页性能稳定不适合直接跳页无限滚动、大数据列表

实际项目中,可以遵循几个原则:

第一,小数据量不要过度优化。

几十万数据、用户只浏览几十页时,普通分页完全可以满足需求。

第二,发现深分页问题后先看执行计划。

例如:

EXPLAIN
SELECT ...
FROM articles
ORDER BY id DESC
LIMIT 20 OFFSET 1000000;

重点观察:

type
key
rows
Extra

不要在没有执行计划的情况下直接修改索引。

第三,大数据量连续分页优先考虑 Keyset Pagination。

尤其是:

Feed
时间线
订单
消息
日志
文章

这类按照固定顺序不断向后读取数据的场景。

第四,排序必须稳定。

推荐:

ORDER BY created_at DESC, id DESC

而不是只依赖一个可能重复的时间字段。

第五,Keyset Pagination 的索引必须和查询条件匹配。

分页优化最终仍然是:

SQL
 ↓
索引
 ↓
执行计划
 ↓
实际数据量
 ↓
性能验证

而不是单纯把:

LIMIT 20 OFFSET 1000000

替换成:

WHERE id < ?

就结束了。

10. 总结

MySQL 深分页的核心问题,可以概括为:

OFFSET 分页
    ↓
OFFSET 越大
    ↓
需要跳过的数据越多
    ↓
查询成本增加

解决方案则可以分成三个层次:

普通分页
    ↓
数据量较小时直接使用

延迟关联
    ↓
减少深分页过程中的回表成本

Keyset Pagination
    ↓
通过索引值直接定位分页位置

其中,Keyset Pagination 是大数据量连续分页场景下更值得优先考虑的方案。

但分页优化不能脱离具体业务。是否需要优化、选择哪种方案,都应该建立在实际数据量、查询方式、索引设计以及 EXPLAIN 分析的基础上。

真正成熟的分页设计不是追求某一种“最优写法”,而是在:

业务体验
+
SQL 复杂度
+
索引成本
+
数据规模
+
分页方式

之间取得合理平衡。