大家好,我是小米,31岁,依旧是个爱折腾数据库的程序员。今天想跟大家聊聊我在一次社招面试中遇到的一个问题 —— “MySQL LIMIT 分页怎么优化?”
故事开头:面试官的微笑
那天我去面试,面试官问:“小米啊,如果一个表有几百万条数据,我们要做分页查询,用 LIMIT offset, size,你会怎么优化?”
我当时心里咯噔一下,毕竟 LIMIT 我平时用得多,但真说到优化,还是得掰开揉碎讲出来才行。于是我就从 LIMIT 的本质问题 开始说起。
面试官笑了笑:“好,那你先讲讲 LIMIT 的缺点吧。”
LIMIT 的“慢病”
很多小伙伴可能习惯写这样的 SQL:
看上去没啥问题,分页嘛,第 100001 条开始取 20 条。但是,你知道 MySQL 内部是怎么干的吗?
它其实是从头开始扫,先把前 100000 条丢掉,再返回后 20 条。
换句话说,LIMIT 的偏移量越大,丢弃的数据越多,性能就越差。 想象一下:
- 你点开外卖 APP,翻到第 1 页,嗖的一下很快。
- 翻到第 5000 页?不好意思,外卖都凉了还没查出来。
所以,LIMIT 在大数据量分页时,性能就是一个灾难。
那该怎么办呢?我总结了几个方法,也正是我在面试时的回答。
记录上次查询的最大 ID(最常用)
这招简单粗暴:我们不要让数据库丢弃大量数据,而是用一个“锚点”。
比如第一页查询时:
假设返回的最后一条记录 ID 是 120。那么第二页我们就可以这样查:
这样,数据库只需要从 ID > 120 开始往后找,不用再数前面几十万条。
是不是很爽?这种方法通常叫 基于游标的分页(Keyset Pagination) 。
优点:
- 查询效率高,尤其是大数据量时几乎不受影响。
- ID 有索引,直接走索引范围扫描,非常快。
缺点:
- 必须依赖一个 唯一且递增的字段(一般是主键 ID) 。
- 不支持“跳页”,比如你直接想看第 500 页,就比较麻烦。
但大多数场景,用户都是“下一页、下一页”往下翻,这个优化非常实用。
覆盖索引优化
有些场景你可能必须要用 OFFSET,比如用户点开页面直接跳转第 N 页。
这时可以这样优化:
先用覆盖索引(只查 ID,不查所有字段),再通过 ID 回表:
这样做的好处是:
- 减少了 MySQL 在 OFFSET 阶段需要处理的数据量。
- 利用索引,避免了全表扫描。
虽然还是会丢弃很多数据,但比起全字段扫描要快得多。
延迟关联
这是上一种方式的进一步改进。先查 ID:
再把 ID 拿去和主表做关联:
好处是,MySQL 在处理大 OFFSET 时,依旧只需要扫 ID 索引,不会动用大字段,性能明显提升。
借助业务逻辑(缓存 & 限制页数)
有时候,技术优化不如“限制需求”来得直接。比如:
- 限制最多翻到第 100 页,超过就不让查。
- 对热门数据(比如最新的前几页)做缓存,避免重复分页查询。
这种方案虽然“土”,但在业务系统里非常常见。
面试中的深入追问
说到这里,面试官点点头,然后问了个关键问题:
“那如果这个表不是按照 ID 排序的,而是按照 create_time 呢?还能用你说的最大 ID 优化吗?”
我心里一紧,但立刻想到了解法。
如果是 create_time 排序,可以记录上一次分页的最后一个时间点,然后加上 ID 来避免时间重复,比如:
这样就能避免时间相同导致的漏查或重复问题。
面试官笑了笑:“不错,继续。”
真实踩坑经历
说实话,我在一个项目里就栽过 LIMIT 的坑。我们有个日志查询页面,用户可以翻到很后面去查历史记录。
一开始我们用的就是最简单的:
结果到了 10 万页的时候,SQL 慢到爆炸,服务器 CPU 飙升。后来我们改成 记录上次 ID 的分页,性能直接起飞,从 20 秒降到 50 毫秒以内。
所以我特别想跟大家说:
- LIMIT 是好用的,但别滥用。
- 大数据量下,一定要学会用 Keyset Pagination。
总结
回顾一下:
- LIMIT 的问题:偏移量大时会扫描并丢弃大量数据,效率极低。
- 优化方式:
- 基于游标的分页(记录最大 ID)。
- 覆盖索引 + 延迟关联。
- 利用业务逻辑限制页数或加缓存。
扩展场景:排序字段不是 ID 时,可以组合时间戳 + 主键做锚点。
最后,面试官笑着说:“小米,你这回答挺全面的。”
我心里一松,感觉这关算是过了。
END
写到这里,我想说:技术的本质是为业务服务。分页查询这种小问题,看似平常,但在高并发、大数据量系统里,真的能决定一个页面是“秒开”还是“崩溃”。
所以,下次你再写 LIMIT,别忘了想想:是不是可以换一种方式更高效?
我是小米,一个喜欢分享技术的31岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!