一、真实生产惨案:接口越翻页越慢,差点背锅
很多后端开发者都遇到过同样的问题:接口刚上线跑得飞快,数据量一上来就卡成PPT。
某后端团队就栽在了一个简单的分页接口上。用户查询交易记录,刚开始响应只要200毫秒,随着数据涨到百万级,直接飙到2~3秒,页面一翻页就卡顿,用户投诉不断。
一开始所有人都以为是数据库压力大、少加索引、服务器不够用。直到他们拿出执行计划一查,才发现罪魁祸首居然是最常用的 OFFSET 分页。
这不是个例,而是90%后端都踩过的坑:你以为的常规写法,在大数据量下就是性能杀手。
很多人天天写分页,却从来没想过数据库底层在做什么。等线上出问题才慌了神,这篇文章,就是帮你提前避开这个大坑。
二、核心拆解:OFFSET为什么慢?正确写法是什么
1. 传统OFFSET分页的致命问题
他们原来的分页SQL大概是这样:
SELECT * FROM transactionWHERE user_id = 123ORDER BY created_at DESCOFFSET 10000 LIMIT 20;看上去非常标准、干净。
但数据库的真实操作是:
- 从磁盘读出 10020 条数据
- 在内存里处理前 10000 条
- 直接扔掉,只返回最后 20 条
页码越靠后,OFFSET越大,数据库要扫描、丢弃的数据就越多,速度自然雪崩。
2. 终极优化:Keyset 分页(键集分页)
真正解决问题的,是改成基于唯一键的跳页查询,也叫seek分页。
改造后SQL:
SELECT * FROM transactionWHERE user_id = 123AND created_at < '上一页最后时间'ORDER BY created_at DESCLIMIT 20;逻辑完全变了:
不再是“跳过前10000条”,而是“从这个时间点之后,给我取20条”。
数据库直接通过B树索引定位位置,不再全量扫描,速度直接起飞。
3. 解决重复时间问题
生产中会出现同一时间多条数据,只用时间会重复或漏数据。
最终稳定版:
SELECT * FROM transactionWHERE user_id = 123AND (created_at < '上次时间' OR (created_at = '上次时间' AND id < 上次ID))ORDER BY created_at DESC, id DESCLIMIT 20;用时间+唯一ID双重判断,分页稳如泰山。
4. 优化后效果
接口响应时间:
- 优化前:2~3秒
- 优化后:300毫秒以内
没加服务器、没加缓存、没改架构,只改了一行SQL。
三、辩证思考:没有银弹,不同场景用不同方案
Keyset分页确实强,但不能无脑用,技术方案永远要看业务场景。
1. Keyset 优点很突出
- 大数据量下极快
- 利用索引,几乎无损耗
- 适合移动端、列表滚动
但它也有短板:

- 不能随机跳页(比如直接跳到第7页、第100页)
- 前端需要携带上一页的游标
2. 必须用OFFSET时怎么救?
后台管理系统经常要跳页,这时候可以用覆盖索引+延迟关联:
SELECT t.*FROM ( SELECT id FROM transaction WHERE user_id = 123 ORDER BY created_at DESC OFFSET 10000 LIMIT 20) tmpJOIN transaction t ON tmp.id = t.id;先在索引里查ID,再回表查数据,比直接OFFSET快很多。
3. 报表类场景:物化视图
统计报表、大屏查询,可以用物化视图,定时刷新结果,彻底解放主库压力。
真正的高级工程师,不是只会一种最优解,而是在对的场景用对的方案。
四、现实意义:性能优化,未必都要堆机器
这次优化给整个团队敲了一个警钟:
性能差,不一定是服务器不够、架构不行。
很多公司一卡就加机器、加缓存、上分布式,成本翻几倍,问题却没解决。
而真正的根因,可能只是一段写得不够优雅的SQL。
这也是最扎心的地方:
你天天加班救火,却不愿意花半小时看懂分页原理。
你花大价钱升级硬件,却不肯优化一句最基础的查询。
线上稳定、用户体验、成本控制,往往都藏在这些细节里。
五、评论互动:你被OFFSET分页坑过吗?
- 你项目里现在用的是 OFFSET 还是 Keyset 游标分页?
- 有没有遇到过分页越翻越慢的线上事故?
- 你还有哪些SQL优化神技巧?
评论区聊聊,帮更多后端避开这个经典大坑。