滚动新闻 →
美俄乌三国代表齐聚阿联酋 重启和平谈判 黄乐被揭受陈一新指挥 中共染指美AI产业 美智库曝共军集结西太平洋 或模拟封锁台湾 下届总统大选生变? 民主党掌多州议会埋伏笔 古代文学中的江南究竟是什么样的 黄乐丈夫康伟身份遭曝光 涉及英伟达订单调查 加拿大对伊朗11家实体重启制裁 黄仁勋婚外情事件揭示中共间谍渗透新手段 中国大陆男子驾车冲撞商场并撞击华为门店 领导伊朗战争遭同僚弹劾 赫格塞斯:一场闹剧 黄仁勋婚外情牵出间谍疑云 中共被指渗透科技界 传统中医如何理解穴位 书画作品上的印章应该怎样看 云南男子抢劫致死少女 服刑期短于从犯引争议 美俄中三方峰会分歧凸显:川普主张即兴对话 习方坚持预设议程 如何教育孩子不要把自己的情绪发泄给别人 西兰花益处多 但一常见做法会让营养白白流失 太离谱!比亚迪电动车轮毂前后装反 众多车主投诉 四川男子家中火灾致现金损毁 银行拒兑残损币 什么情况下应该使用慢节奏剪辑 沪漂白领失业转型:AI冲击下高收入群体面临生存困境 黄仁勋陷入间谍疑云 赫格塞斯面临弹劾 美国科技安全与总统战争权力同时受到审视 山东菏泽街头汽车冲撞事件 至少9人死伤 美国10月CPI涨幅超预期 美联储或再度加息 美国对伊朗战争接近尾声 卢比奥称经济制裁已显成效 英伟达股东集体诉讼黄仁勋 股价单日暴跌4% 清朝灭亡后中国为什么没有立即实现稳定 AMD第五代EPYC处理器被Google TPU 8tx采用 冲击NVIDIA市场主导地位 美军海外基地紧急撤离 美国推迟对伊军事行动 中共‘美人计’渗透政商高层数十年未止 赫格塞斯回应万斯伊朗战争言论 弹劾风波再起 葡萄牙面临风暴'查尔斯'袭击 全国进入警戒状态 黄仁勋事件折射日本政界遭遇中共情报渗透 佛罗里达州启动调查聚焦左翼组织与中共古巴资金关联 小阳台如何兼顾晾晒和收纳 黄乐与康伟的资本网络浮出水面 英伟达芯片流向背后还有多少未解之谜 川普称中东盟友要求维持对伊朗海陆封锁 绘架座β星神秘尘埃结构或源于天体碰撞 中国多病毒疫情并发 年轻人梗栓风险升高 中共全面从严治党剑指官场‘老好人’ 习近平强化党内监督

MySQL LIMIT 分页为什么越翻越慢?大数据量网站可以怎样优化

发布时间: 2026-11-10 18:00:02    最后更新: 2026-11-10 18:12:43    阅读:6  约15 分钟阅读     

MySQL LIMIT 分页为什么越翻越慢?大数据量网站可以怎样优化
图片说明:示意图   图片来源:Public Domain(公有领域)
一个网站刚上线时,分页通常感觉不到任何问题。比如文章表只有几万条数据,执行 LIMIT 0, 20、LIMIT 20, 20,查询都可能非常快。但当数据库积累到几百万甚至上千万条记录以后,用户如果一路翻到第5000页,情况就完全不同了。

例如一个新闻网站每页显示20篇文章:

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

用户真正需要的只有20条记录,但数据库为了得到这20条结果,通常仍然需要按照执行计划处理前面的100000条候选记录,然后再取后面的20条。随着 OFFSET 增大,查询工作量通常也会增加。它不是简单的“指数级变慢”,更准确地说,是随着偏移位置扩大而出现明显的性能衰减,而且最终速度还受到索引、排序、回表、磁盘IO、缓存命中率和查询条件等因素影响。

一、为什么 OFFSET 越大越容易慢

假设网站有100万篇文章,每页20篇。

第一页执行:

LIMIT 0, 20

数据库只需要找到最前面的20条结果。

如果用户打开第50000页:

LIMIT 999980, 20

这时候真正返回的仍然只有20条,但为了定位这20条,执行过程可能需要处理接近100万条候选记录。

如果 ORDER BY created_at DESC 有合适索引,数据库可以沿着索引扫描,而不是把整个表重新排序;但“沿索引向前走”并不意味着可以瞬间跳到第999980个符合条件的记录。对于 B+Tree 索引,定位索引起点和顺序扫描大量索引项是两回事。

更麻烦的是:

SELECT *
FROM news
ORDER BY created_at DESC
LIMIT 999980, 20;

如果索引无法覆盖查询需要的字段,数据库还可能产生大量回表访问。此时查询成本不仅来自 OFFSET 本身,还来自大量候选记录的读取。

因此,看到 LIMIT 100000,20 很慢时,不能简单地说“LIMIT 有问题”,而应该先看 EXPLAIN,确认数据库到底是在扫描什么、排序什么、回表多少。

二、真正适合大数据分页的是 Keyset Pagination

如果网站只是上一页、下一页,通常没有必要让数据库每次从第一页开始数。

例如新闻网站按照:

ORDER BY created_at DESC, id DESC

排序。

第一次请求直接:

SELECT id, title, created_at
FROM news
WHERE publish_status = 1
ORDER BY created_at DESC, id DESC
LIMIT 20;

返回结果后,程序记住最后一条记录的:

created_at = 2026-10-07 12:30:00
id = 381245

下一页就可以写成:

SELECT id, title, created_at
FROM news
WHERE publish_status = 1
AND (
created_at < '2026-10-07 12:30:00'
OR (
created_at = '2026-10-07 12:30:00'
AND id < 381245
)
)
ORDER BY created_at DESC, id DESC
LIMIT 20;

这就是 Keyset Pagination,也经常被称为 Cursor Pagination。

它真正解决的问题不是“把 OFFSET 换成另一个参数”,而是告诉数据库下一页应该从哪里继续。

如果索引设计正确,数据库可以从游标所在的位置继续向后扫描,而不需要为了寻找第50001页,再处理前面的大量结果。

这也是为什么新闻流、评论流、订单列表、消息列表等“只需要继续往后看”的业务,非常适合这种分页方式。

三、为什么不能简单使用 id > last_id

很多教程会直接写:

WHERE id > last_id
ORDER BY created_at DESC
LIMIT 20;

这实际上存在排序逻辑不一致的问题。

假设:

id created_at
100 10:00
101 09:59
102 09:58

数据库按照 created_at DESC 排序,但分页条件却按照 id > last_id 判断,两者并不是同一个排序体系。

更合理的方法是让游标和排序字段保持一致。例如:

ORDER BY created_at DESC, id DESC

那么下一页就应该使用:

(created_at, id) < (:last_created_at, :last_id)

在 MySQL 中也可以使用展开后的条件:

WHERE created_at < :last_created_at
OR (
created_at = :last_created_at
AND id < :last_id
)

这里把 id 放在最后,是为了处理多个记录拥有相同时间戳的情况。

例如一秒钟内同时发布了20篇文章,如果只使用:

WHERE created_at < :last_created_at

就可能漏掉与游标具有相同时间戳的记录。

因此,实际系统中的分页游标通常需要一个稳定的排序键组合。

四、索引必须跟着查询方式设计

对于上面的新闻查询,如果经常执行:

WHERE publish_status = 1
ORDER BY created_at DESC, id DESC
LIMIT 20

就应该根据实际数据分布和执行计划设计复合索引,例如:

CREATE INDEX idx_news_status_time_id
ON news (publish_status, created_at, id);

这样数据库可以利用索引处理过滤和排序。

但索引设计没有一个适用于所有网站的固定答案。假如查询条件、排序方式或者数据分布发生变化,最佳索引也可能变化。

例如另一个查询是:

WHERE category_id = 5
AND publish_status = 1
ORDER BY created_at DESC, id DESC
LIMIT 20;

它需要考虑的索引结构就与前面的查询不同。

因此不要看到某个字段经常出现在 WHERE 中,就机械地给它建立单列索引。更重要的是把真实查询拿出来,用 EXPLAIN 或 EXPLAIN ANALYZE 检查执行计划。

五、覆盖索引很有用,但不要把它理解成万能加速器

所谓覆盖索引,是指查询需要的列已经全部包含在索引中,数据库可以直接从索引读取结果,不必为了这些列回到聚簇索引记录中。

例如:

SELECT id, created_at
FROM news
WHERE publish_status = 1
ORDER BY created_at DESC, id DESC
LIMIT 20;

如果索引能够覆盖这些字段,查询可能非常高效。

但是,如果页面需要:

SELECT id, title, content, author, image, created_at

把这些字段全部塞进索引未必是好主意。索引越宽,占用空间越大,写入成本也越高。

实际网站更常见的做法是先用索引快速找到20个ID,然后再根据这些ID获取完整记录。

例如:

SELECT id
FROM news
WHERE publish_status = 1
ORDER BY created_at DESC, id DESC
LIMIT 20;

然后:

SELECT id, title, content, author, image, created_at
FROM news
WHERE id IN (...);

这种“两阶段查询”在某些宽表、复杂排序和大数据量场景下很有价值,但是否更快仍然应该通过实际执行计划和基准测试确认。

六、不是所有分页都应该改成 Cursor

如果用户的网站需要直接输入:

第1页
第50页
第100页
第500页

那么 Keyset Pagination 就没有 OFFSET 那么方便。

例如电商后台管理员可能要求直接跳到第37页。这种需求本身就需要定位一个绝对位置。

因此实际系统通常可以采用混合策略。

普通用户浏览新闻:

上一页 / 下一页

采用 Cursor。

后台管理系统:

第1页、第2页……第N页

可以继续使用 OFFSET,但限制最大可跳页数。

对于非常深的页面,则可以要求用户使用搜索条件、日期范围、分类、状态等缩小结果集,而不是让数据库承担:

LIMIT 5000000, 20

这样的深分页。

七、缓存不能解决错误的分页算法

Redis 可以缓存热门查询,但不能因此认为:

LIMIT 很慢 → Redis 全部缓存 → 问题解决

例如首页第一页每天有几十万次访问,把热门结果缓存起来非常合理。

但如果用户不停访问:

第1页
第2页
第3页
……
第5000页

缓存每一个分页结果,反而可能制造大量低命中率缓存。

对于动态新闻网站,更合理的做法通常是缓存热点页面、热点分类、热门文章ID列表等,而不是机械地缓存所有 OFFSET 分页。

同时还要考虑缓存失效。文章发布、删除、置顶、分类变化后,相关缓存是否及时更新,往往比“有没有使用Redis”更加重要。

八、数据量特别大时再考虑分片

Sharding 可以解决单表、单库规模继续增长的问题,但它不是 LIMIT 分页的第一优化手段。

如果一张表只有几百万行,却因为:

LIMIT 100000,20

查询很慢,就直接上分库分表,通常属于架构过度。

应该先解决:

查询条件
索引
排序
Keyset Pagination
返回字段
回表
缓存

这些基础问题。

只有当单机数据库的存储容量、写入能力、查询压力或者业务隔离需求真正达到瓶颈时,才应该考虑 Sharding。

而一旦分片,跨分片排序、全局分页、全局唯一ID、聚合查询都会变得更加复杂。

九、EXPLAIN 才是判断分页到底慢在哪里的工具

不要看到 LIMIT 很大就直接修改SQL。

例如:

EXPLAIN
SELECT id, title, created_at
FROM news
WHERE publish_status = 1
ORDER BY created_at DESC, id DESC
LIMIT 100000, 20;

重点应该观察:

key
type
rows
Extra

以及在需要的时候使用:

EXPLAIN ANALYZE

观察实际执行情况。

type = ALL 通常意味着全表扫描,但不能简单地把任何 ALL 都视为错误;小表、特殊查询或者优化器判断全表扫描成本更低时,都可能出现这种执行计划。

同样,Using filesort 也不等于“查询一定很慢”。它表示排序没有完全通过索引顺序完成,是否构成实际瓶颈还要结合数据量、排序规模和执行时间判断。

十、真正的大数据分页优化是什么

假设一个新闻网站已经有几千万篇文章,首页只需要最新20篇。

低效方案可能是:

SELECT *
FROM news
ORDER BY created_at DESC
LIMIT 1000000, 20;

更合理的方向是:

SELECT id, title, created_at
FROM news
WHERE publish_status = 1
ORDER BY created_at DESC, id DESC
LIMIT 20;

下一页使用上一页最后一条记录作为游标:

SELECT id, title, created_at
FROM news
WHERE publish_status = 1
AND (
created_at < :created_at
OR (
created_at = :created_at
AND id < :id
)
)
ORDER BY created_at DESC, id DESC
LIMIT 20;

然后针对这个查询建立与实际业务匹配的复合索引,再用 EXPLAIN ANALYZE 验证。

如果页面还需要大量正文、图片和其他字段,可以进一步采用“先取ID,再批量获取完整记录”的方式。

这才是从数据库执行层面减少无意义工作的办法。

MySQL 分页优化的核心,并不是简单地把 LIMIT 换成某一个所谓的“高级分页插件”,也不是给表疯狂增加索引。真正需要解决的是一个更具体的问题:用户只需要20条数据,数据库有没有被迫为了找到这20条数据而处理几十万、几百万甚至更多的无关记录。

如果业务允许顺序翻页,Keyset Pagination 通常是大数据分页最值得优先考虑的方案;如果业务必须支持任意页码,就需要接受 OFFSET 深分页的成本,并通过索引、查询条件、最大页码、缓存和数据范围限制控制它。

当数据从几万增长到几百万、几千万以后,最有价值的优化往往不是“换一个更快的LIMIT写法”,而是重新设计分页所依据的排序键、索引和查询路径。让数据库从正确的位置继续寻找下一页,而不是每次都从头数到目标位置,这才是大数据分页真正的优化方向。

喜欢这篇报道?

使用下面的功能,方便以后继续阅读和分享 MNewsTV

设为 Google 新闻首选来源 让 Google 新闻优先显示 MNewsTV 的最新报道 ›
★ 我的收藏 查看已经收藏的文章
关于文章收藏 收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。 删除收藏请进入「我的收藏」进行管理。
分享这篇报道

捐助(Paypal): https://www.paypal.me/observeccp
订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP