qinyelin
发布于 2026-09-08 / 0 阅读
0
0

MySQL LIMIT深分页优化

MySQL LIMIT 深分页优化

面试频率:★★★★★
工作频率:★★★★★


⚡ 30 秒速记

普通分页:

SELECT *
FROM orders
ORDER BY id
LIMIT 0, 10;

深分页:

SELECT *
FROM orders
ORDER BY id
LIMIT 5000000, 10;

很多人会误以为:

直接跳到第500万条
↓
取10条

实际上不能这么理解。

LIMIT 5000000, 10 可以简单理解为:

扫描 / 处理前5000010条
        ↓
丢掉前5000000条
        ↓
返回最后10条

所以:

OFFSET 越大,需要扫描和丢弃的前置记录越多,深分页通常越慢。

常见优化:

方案1:覆盖索引 + 延迟关联

先通过索引找到最终需要的主键
↓
再根据主键获取完整数据
↓
减少昂贵的数据访问

更好的方案:

方案2:游标分页

WHERE id > lastId
ORDER BY id
LIMIT 10

核心:

大 OFFSET
↓
扫描大量无用数据


游标分页
↓
通过索引直接定位
↓
继续向后读取

一、什么是分页?

实际开发非常常见:

SELECT *
FROM orders
LIMIT 0, 10;

表示:

从第0条开始
↓
取10条

下一页:

SELECT *
FROM orders
LIMIT 10, 10;

再下一页:

SELECT *
FROM orders
LIMIT 20, 10;

语法:

LIMIT offset, size;

其中:

offset
=
跳过多少条


size
=
返回多少条

例如:

LIMIT 100, 20;

表示:

跳过前100条
↓
返回20条

二、什么是深分页?

例如:

SELECT *
FROM orders
ORDER BY id
LIMIT 10, 10;

offset:

10

很小。

但是:

SELECT *
FROM orders
ORDER BY id
LIMIT 5000000, 10;

offset:

5000000

非常大。

这种:

OFFSET非常大的分页

通常就叫:

深分页

三、为什么 LIMIT 深分页慢?

假设:

LIMIT 5000000, 10;

不要理解成:

MySQL瞬间跳到第500万条
        ↓
读取10条

可以简单理解成:

找到 / 扫描 / 处理前5000010条
            ↓
丢掉前5000000条
            ↓
只返回最后10条

也就是说:

前500万条

虽然:

最终不会返回给客户端

但数据库仍然需要处理大量前置记录。

所以:

OFFSET越大
↓
需要跳过的数据越多
↓
查询通常越慢

四、举个最简单的例子

假设:

orders表
=
1000万条数据

查询:

SELECT *
FROM orders
ORDER BY id
LIMIT 0, 10;

只需要:

从前面开始
↓
读取10条
↓
停止

非常快。


但是:

SELECT *
FROM orders
ORDER BY id
LIMIT 5000000, 10;

需要:

处理大量前置记录
↓
跳过500万条
↓
再留下10条

所以:

第1页

和:

第50万页

即使:

每页都是10条

性能也可能完全不一样。


五、为什么有索引也可能慢?

很多人会问:

id 是主键,有索引,为什么 LIMIT 5000000,10 还是慢?

因为索引可以帮助:

快速查找某个确定的值 / 范围

例如:

WHERE id = 5000000;

B+ Tree 可以:

直接定位

但是:

LIMIT 5000000, 10;

表达的是:

跳过结果集前500万条

它没有直接告诉 MySQL:

从哪个id开始

所以:

有索引

并不代表:

大OFFSET没有成本

六、深分页和 B+ Tree 的关系

假设主键索引:

1
2
3
4
5
...
5000000
5000001
5000002
...

执行:

SELECT *
FROM orders
WHERE id > 5000000
ORDER BY id
LIMIT 10;

这里有明确条件:

id > 5000000

B+ Tree 可以:

定位到5000000附近
↓
向后扫描
↓
读取10条
↓
结束

但是:

SELECT *
FROM orders
ORDER BY id
LIMIT 5000000, 10;

没有:

id > xxx

这样的索引定位条件。

所以:

OFFSET分页

和:

索引范围查询

不是一回事。


七、深分页遇到二级索引

假设:

CREATE INDEX idx_create_time
ON orders(create_time);

查询:

SELECT *
FROM orders
ORDER BY create_time
LIMIT 5000000, 10;

二级索引可以简单理解:

create_time
+
主键id

而:

SELECT *

需要:

完整订单数据

完整数据在:

聚簇索引

里面。

因此最终可能涉及:

二级索引扫描
      ↓
获得主键
      ↓
根据执行计划获取完整行
      ↓
大量前置记录最终又被OFFSET丢弃

这种场景下:

大OFFSET
+
完整行访问成本

可能让查询更加昂贵。

注意:

具体执行方式最终由优化器决定,不能简单认为所有深分页 SQL 都一定会对 OFFSET 前的每一行进行同样形式的回表。

但优化思想是一样的:

尽量不要为了最终不要的数据
获取大量完整行

八、优化方案一:覆盖索引 + 延迟关联

原 SQL:

SELECT *
FROM orders
ORDER BY create_time
LIMIT 5000000, 10;

可以考虑:

SELECT o.*
FROM orders o
JOIN (
    SELECT id
    FROM orders
    ORDER BY create_time
    LIMIT 5000000, 10
) t
ON o.id = t.id
ORDER BY o.create_time;

这里最重要的不是:

死记这条SQL

而是理解:

为什么这么做

九、延迟关联原理

假设索引:

(create_time)

InnoDB 二级索引叶子记录可以简单理解:

create_time + 主键id

子查询:

SELECT id
FROM orders
ORDER BY create_time
LIMIT 5000000, 10;

需要:

create_time
+
id

这些信息:

索引中就有

所以有机会:

通过覆盖索引扫描

先得到最终:

10个id

然后:

10个id
↓
JOIN主表
↓
获取10条完整数据

核心思想:

第一阶段:

只处理较小的索引记录
↓
完成OFFSET


第二阶段:

只对最终需要的数据
↓
获取完整行

十、为什么叫“延迟关联”?

因为:

获取完整数据

这个动作:

被推迟了

原来的思路:

扫描候选数据
↓
获取完整行
↓
OFFSET丢弃大量数据
↓
留下10条

优化思路:

先通过索引
↓
找到最终10个主键
↓
最后才关联主表
↓
获取10条完整数据

所以叫:

延迟关联

可以记成:

能晚点拿完整行,就不要太早拿完整行。


十一、它和覆盖索引有什么关系?

你之前已经学过:

覆盖索引

核心:

查询需要的数据
↓
索引里面全部都有
↓
不需要回表

延迟关联就是利用这个思想:

第一阶段
↓
只查询id
↓
尽量利用覆盖索引
↓
完成深分页

然后:

第二阶段
↓
根据最终少量id
↓
获取完整数据

所以:

覆盖索引
+
延迟关联

是深分页经典优化方式之一。


十二、但是 OFFSET 本身消失了吗?

没有。

这是非常重要的。

即使:

SELECT id
FROM orders
ORDER BY create_time
LIMIT 5000000, 10;

MySQL 仍然:

需要处理大量索引记录

因为:

OFFSET还是5000000

所以延迟关联:

不是让500万OFFSET消失

而是:

让处理这 500 万条前置记录的成本尽量变低。

也就是:

原来:

处理大量完整行

变成:

尽量只处理较小的索引记录

所以:

延迟关联
=
降低深分页成本

而不是:

彻底消灭深分页

十三、更好的方案:游标分页

如果业务允许:

不要使用大OFFSET

可以记录:

上一页最后一条记录

例如上一页:

id

10001
10002
10003
...
10010

最后:

lastId = 10010

下一页:

SELECT *
FROM orders
WHERE id > 10010
ORDER BY id
LIMIT 10;

这就是:

游标分页

也经常叫:

Keyset Pagination

十四、游标分页为什么快?

因为:

WHERE id > 10010

是:

索引范围查询

主键 B+ Tree:

...
10008
10009
10010
10011
10012
10013
...

MySQL:

通过B+ Tree
↓
定位到10010附近
↓
从10011继续扫描
↓
读取10条
↓
停止

不需要:

从结果集开头
↓
跳过前面几百万条

所以:

OFFSET分页

LIMIT 5000000,10

↓

大量前置数据被扫描 / 丢弃

而:

游标分页

WHERE id > lastId
LIMIT 10

↓

索引定位
↓
向后读取10条

十五、两种分页对比

OFFSET 分页

SELECT *
FROM orders
ORDER BY id
LIMIT 5000000, 10;

流程:

处理大量前置记录
↓
丢掉500万条
↓
返回10条

特点:

可以直接跳页

例如:

第1页
第100页
第50000页

但是:

页数越深
↓
通常越慢

游标分页

SELECT *
FROM orders
WHERE id > 5000000
ORDER BY id
LIMIT 10;

流程:

通过索引定位
↓
向后扫描
↓
取10条

特点:

深度增加以后
性能通常更加稳定

但是:

需要知道上一页游标

十六、游标分页的缺点

游标分页并不是所有业务都适合。

例如前端:

1
2
3
4
5
...
10000

用户点击:

第8000页

如果使用:

WHERE id > lastId

你需要知道:

第7999页最后一个id

所以:

任意跳页

不是游标分页最擅长的场景。


十七、哪些业务适合游标分页?

非常适合:

朋友圈
微博 / 信息流
短视频列表
订单列表的“下一页”
日志查询
加载更多
无限滚动

因为用户操作:

第一批
↓
下一批
↓
下一批
↓
继续加载

不需要:

直接跳到第50000页

十八、如果按照时间排序怎么办?

实际业务经常:

SELECT *
FROM orders
ORDER BY create_time DESC
LIMIT 20;

下一页不能永远只依赖:

id > lastId

因为:

真正排序字段是create_time

应该让:

游标条件

与:

ORDER BY

保持一致。

例如:

create_time

如果可能重复,实际设计中经常使用:

create_time + id

作为稳定排序键。

例如:

ORDER BY create_time DESC, id DESC

索引可以考虑:

INDEX(create_time DESC, id DESC)

游标也应该基于:

上一页最后一条的create_time
+
上一页最后一条的id

继续查询。

核心原则:

游标分页的游标,要和排序规则对应。


十九、为什么经常加 id?

假设:

create_time

只有:

2026-09-08 10:00:00

可能:

100条订单

时间完全一样。

如果只按照:

create_time

分页:

排序可能不够唯一

所以:

create_time
+
id

可以形成:

稳定、唯一的排序顺序

例如:

ORDER BY create_time DESC, id DESC

这样分页更加稳定。


二十、深分页和 ORDER BY 的关系

之前已经学过:

ORDER BY索引优化

深分页 SQL:

SELECT *
FROM orders
ORDER BY create_time
LIMIT 5000000, 10;

如果:

create_time没有合适索引

还可能出现:

Using filesort

那么问题就可能变成:

大量数据
+
额外排序
+
大OFFSET

性能可能更差。

所以分析深分页:

不仅看LIMIT

还要看:

ORDER BY能不能利用索引

二十一、深分页和覆盖索引的关系

原 SQL:

SELECT *
FROM orders
ORDER BY create_time
LIMIT 5000000, 10;

SELECT *

需要完整行

优化:

SELECT id
FROM orders
ORDER BY create_time
LIMIT 5000000, 10;

如果:

create_time + id

都可以从索引获得:

覆盖索引

就可以:

先利用较小的索引记录
↓
完成深分页
↓
得到少量id
↓
再获取完整行

所以你之前学的:

覆盖索引

在这里直接派上用场。


二十二、深分页和回表的关系

二级索引:

create_time + id

完整数据:

聚簇索引

如果查询:

SELECT *

最终:

可能需要获取完整行

而深分页优化的重要思路之一就是:

不要过早获取大量完整行

尽量:

先在二级索引中
↓
找到最终需要的数据
↓
再取完整行

所以:

深分页
↓
覆盖索引
↓
延迟关联
↓
减少昂贵的完整行访问

这些知识是连在一起的。


二十三、深分页和 EXPLAIN

遇到:

SELECT *
FROM orders
ORDER BY create_time
LIMIT 5000000, 10;

不要直接猜。

使用:

EXPLAIN
SELECT *
FROM orders
ORDER BY create_time
LIMIT 5000000, 10;

重点看:

type

访问方式。

key

实际用了哪个索引。

rows

预计需要检查多少记录。

Extra

看看有没有:

Using filesort

等信息。

所以你之前学的:

EXPLAIN

就是拿来验证:

SQL到底怎么执行

而不是:

只靠背优化规则

二十四、实际开发怎么选?

可以简单分三种情况。

情况一:数据量不大

例如:

几千条

普通:

LIMIT offset, size

完全可以。

不要为了:

几千条数据

把代码搞得特别复杂。


情况二:需要任意跳页

例如:

1 2 3 4 5 ... 1000

业务要求:

直接跳第500页

可以继续使用:

OFFSET分页

如果深分页性能出现问题:

考虑覆盖索引
+
延迟关联

情况三:只需要下一页 / 加载更多

优先考虑:

游标分页

例如:

WHERE id > lastId
ORDER BY id
LIMIT 20;

通常比:

LIMIT 5000000, 20

更加适合。


二十五、三种方案总结

普通OFFSET分页

优点:
简单
支持跳页

缺点:
深分页慢

覆盖索引 + 延迟关联

优点:
降低大OFFSET下获取完整行的成本
仍可以保留OFFSET分页

缺点:
OFFSET本身仍然存在
SQL更加复杂

游标分页

优点:
避免大OFFSET
利用索引直接定位
深分页性能通常更稳定

缺点:
不方便任意跳页
需要维护游标

二十六、一张图记住深分页

LIMIT 5000000, 10
        ↓
处理大量前置记录
        ↓
丢弃500万条
        ↓
返回10条
        ↓
OFFSET越大
        ↓
通常越慢

优化一:

覆盖索引
   ↓
先查最终需要的id
   ↓
延迟获取完整行
   ↓
降低深分页成本

优化二:

WHERE id > lastId
        ↓
B+ Tree直接定位
        ↓
向后读取10条
        ↓
避免大OFFSET

二十七、和前面的知识全部串起来

现在:

                    MySQL索引
                       ↓
                    B+ Tree
                       ↓
              聚簇索引 / 二级索引
                       ↓
                      回表
                   ↙       ↘
             覆盖索引       ICP
                ↓            ↓
             避免回表      减少回表
                ↓
            深分页优化
                ↓
         覆盖索引 + 延迟关联

另外:

ORDER BY
   ↓
利用索引有序性
   ↓
减少额外排序
   ↓
配合LIMIT

最终:

深分页
   ↓
能不能不用OFFSET?
   ↓
 ┌───────┴───────┐
 ↓               ↓
能               不能
 ↓                ↓
游标分页       覆盖索引
             + 延迟关联

🎤 面试回答

问:

MySQL LIMIT 深分页为什么慢?怎么优化?

答:

MySQL 使用 LIMIT offset,size 进行深分页时,offset 很大并不意味着数据库可以直接跳到目标位置。数据库仍然需要扫描或处理大量 offset 之前的记录,然后将这些记录丢弃,只返回后面的 size 条,所以 offset 越大,查询通常越慢。

如果查询还涉及获取完整行,大量无效数据访问会进一步增加成本。

常见优化方式有两种:第一种是利用覆盖索引先完成分页,只得到最终需要的主键,再通过延迟关联获取完整数据,从而降低完整行访问成本;第二种是如果业务允许,使用基于主键或排序键的游标分页,例如 WHERE id > lastId ORDER BY id LIMIT 10,通过 B+ Tree 直接定位到上一次的位置继续扫描,从根本上避免大 OFFSET。


🎯 面试追问

Q1:什么是深分页?

答:

OFFSET非常大的分页

例如:

LIMIT 5000000, 10;

Q2:LIMIT 5000000,10 是直接跳到第500万条吗?

不是。

可以简单理解:

处理前5000010条
↓
丢掉前5000000条
↓
返回10条

Q3:为什么 OFFSET 越大越慢?

因为:

需要扫描 / 处理
更多前置记录

而这些记录:

最终又会被丢弃

Q4:有主键索引为什么深分页还是可能慢?

因为:

LIMIT 5000000,10

只是:

告诉数据库跳过结果集前500万条

并没有提供:

id > 某个确定值

这样的索引范围定位条件。


Q5:什么是延迟关联?

先:

通过覆盖索引
↓
完成深分页
↓
得到最终少量主键

再:

根据主键
↓
获取完整行

把:

获取完整数据

延迟到:

分页完成以后

Q6:延迟关联能彻底解决 OFFSET 问题吗?

不能。

因为:

OFFSET仍然存在

它主要是:

降低处理大量前置记录时
获取完整行的成本

Q7:什么是游标分页?

例如:

SELECT *
FROM orders
WHERE id > ?
ORDER BY id
LIMIT 10;

记录:

上一页最后一个id

下一页:

从这个id继续

Q8:为什么游标分页快?

因为:

WHERE id > lastId

可以:

利用B+ Tree范围定位
↓
从目标位置继续扫描
↓
只读取需要的数据

避免:

大OFFSET

Q9:游标分页有什么缺点?

主要:

不方便任意跳页

它更适合:

下一页
加载更多
无限滚动

Q10:按照 create_time 排序还能只用 id 做游标吗?

不应该简单这么做。

游标应该:

和ORDER BY规则对应

如果:

ORDER BY create_time DESC, id DESC

那么通常应该基于:

create_time
+
id

继续分页。


Q11:为什么 create_time 经常和 id 一起作为游标?

因为:

create_time可能重复

而:

create_time + id

可以形成:

稳定、唯一的排序顺序

减少分页过程中:

重复
遗漏
顺序不稳定

等问题。


Q12:深分页和覆盖索引有什么关系?

可以:

先只查询索引中的字段
↓
完成OFFSET
↓
得到最终少量主键
↓
再获取完整数据

降低深分页成本。


Q13:深分页和 ORDER BY 有什么关系?

如果:

ORDER BY没有合适索引

还可能:

Using filesort

形成:

大量数据
+
额外排序
+
大OFFSET

性能可能更差。


Q14:怎么分析深分页 SQL?

使用:

EXPLAIN

重点看:

type
key
rows
Extra

同时结合:

实际数据量
实际执行时间

判断。


⚠️ 易错点

1. LIMIT 大 OFFSET 不是直接跳过去

错误:

LIMIT 5000000,10

↓

直接定位第500万条

正确:

需要处理大量前置记录
↓
然后丢弃

2. 有索引 ≠ 深分页一定快

索引可以:

帮助定位
帮助排序

但是:

大OFFSET本身仍然有成本

3. 延迟关联没有消灭 OFFSET

延迟关联:

降低前置记录的处理成本

而不是:

让OFFSET消失

真正避免大 OFFSET 的典型方案:

游标分页

4. 游标分页不一定只能用 id

应该根据:

ORDER BY字段

设计游标。

例如:

ORDER BY create_time DESC, id DESC

通常使用:

create_time + id

作为分页依据。


5. 游标分页不适合所有业务

如果必须:

任意跳到第10000页

游标分页:

不方便

它最适合:

下一页
加载更多
无限滚动

6. 不要看到 LIMIT 就一定优化

例如:

总共只有5000条数据

普通 OFFSET:

完全可能够用

优化一定要结合:

数据量
访问频率
实际执行时间
业务需求

❓ 自测

  1. LIMIT 的两个参数分别表示什么?

  2. 什么是深分页?

  3. 为什么 LIMIT 5000000,10 会比较慢?

  4. MySQL 会直接跳到第500万条吗?

  5. OFFSET 越大为什么通常越慢?

  6. 有主键索引为什么深分页仍然可能慢?

  7. 深分页和 B+ Tree 有什么关系?

  8. 什么是延迟关联?

  9. 为什么延迟关联通常会先查询 id?

  10. 延迟关联和覆盖索引有什么关系?

  11. 延迟关联能彻底消灭 OFFSET 吗?

  12. 什么是游标分页?

  13. WHERE id > lastId 为什么比大 OFFSET 更适合连续翻页?

  14. 游标分页利用了 B+ Tree 的什么能力?

  15. 游标分页最大的缺点是什么?

  16. 哪些业务适合游标分页?

  17. 哪些业务不适合单纯使用游标分页?

  18. 如果按照 create_time 排序,游标应该怎么设计?

  19. 为什么经常使用 create_time + id

  20. 深分页和 ORDER BY 有什么关系?

  21. 如果 ORDER BY 没有索引,可能看到什么?

  22. Using filesort 是什么意思?

  23. 深分页和回表有什么关系?

  24. 深分页和覆盖索引有什么关系?

  25. EXPLAIN 分析深分页时重点看哪些字段?

  26. 普通 OFFSET、延迟关联、游标分页分别有什么优缺点?

  27. 为什么说“延迟关联是降低 OFFSET 成本,而游标分页是避免大 OFFSET”?

  28. LIMIT 0,10LIMIT 5000000,10 为什么性能可能差很多?

  29. 如果业务只需要“下一页”,你更推荐哪种分页?

  30. 如果业务必须任意跳页,应该如何选择分页方案?


评论