百万级数据分页怎么做?深扒 PG 游标分页与传统分页的爱恨情仇

24 阅读5分钟

unnamed.jpg API超时真凶!百万数据扫描0产出!

一睁眼,你的生产环境报警群炸了——接口响应时间超过30秒!

并不是被DDoS攻击,仅仅是一个运营小妹想在后台查看第10000页的历史订单。

就像资深DBA常挂在嘴边的那句狠话: “盲目使用OFFSET分页,就是给数据库判了死缓。”

今天我们就来深扒一下 PostgreSQL(PG)里关于分页的那些“爱恨情仇”,看看你的代码是不是正在裸奔。

最熟悉的陌生人

每一个写过 SQL 的兄弟,大概都是从 LIMIT 10 OFFSET 0 开始入坑的。

这语法太符合直觉了,前台传个 page_no,后台算一下偏移量,代码写起来如丝般顺滑。

但问题来了,数据库真的觉得顺滑吗?

当你执行 OFFSET 1000000 LIMIT 10 时,你以为 PG 只是乖乖跳到了第100万行,然后取了10条数据?

大错特错。

PostgreSQL 的引擎机制决定了,它必须先扫描完前 1000010 行数据,然后把前 1000000 行扔进垃圾桶,只留最后 10 行给你。

什么概念?

为了拿这10条数据,你让 CPU 白白做了10万倍的功。这就是经典的 O(N)O(N) 复杂度陷阱。

随着页码越翻越深,查询时间呈线性爆炸式增长。数据不会撒谎:在百万级数据表中,OFFSET 0 耗时可能只有 1ms,但到了 OFFSET 1000000,耗时可能直接飙升到 5秒甚至更多。

这哪里是查询,简直是在“谋杀”你的 I/O 资源。

像书签一样快

既然硬翻书太累,聪明的工程师想出了**“书签大法” ,也就是我们常说的游标分页(Keyset Pagination)**。

如果你用过 Twitter 或者刷过朋友圈,你会发现它们永远是“下拉加载更多”,而不是让你选“第几页”。

这就是游标分页的主场。

它的原理简单粗暴:不告诉数据库“跳过多少行”,而是告诉它“上一页最后那条数据的 ID 是多少”。

SQL 画风突变:

WHERE id > last_seen_id LIMIT 10

这波操作有多秀?

利用索引(Index),数据库可以直接定位到 last_seen_id 的位置,然后顺手往后拿10条。

无论你的数据是 1 万行还是 1 亿行,无论你翻到第几页,查询复杂度始终稳定在 O(1)O(1)O(logN)O(\log N)

这就好比你看书,OFFSET 是每次都要从第一页数到第五百页;而游标分页,是直接翻到你上次夹书签的那一页。

效率提升不是一倍两倍,而是指数级的降维打击。

没有银弹

听到这,是不是想回去把所有代码重构了?

先别急,技术圈没有银弹。

虽然游标分页在性能上吊打传统分页,但它有两个致命的**“阿喀琉斯之踵”**:

  1. 告别“随机跳转” :你无法直接跳到“第 50 页”。因为你不知道第 49 页的最后一条 ID 是什么。
  2. 实现门槛高:如果你的排序字段不是唯一的(比如按“价格”排序),你还得引入主键 ID 做二级排序,否则分页数据会乱套。

我们来看一个残酷的对比表:

维度LIMIT / OFFSET游标分页
深度分页性能极差(拖垮数据库)极好(毫秒级响应)
实现难度实习生都会需要资深开发设计
随机跳转支持不支持
数据一致性差(翻页间隙新增数据会导致重复)完美(天然适应动态数据)

场景决定生死

所以,到底该选哪一个?这里直接给出一份**“保命决策指南”**:

场景一:后台管理系统

数据量也就几万条,运营人员需要快速跳转到第 10 页查看数据。

结论: 放心用 OFFSET。这点数据量 PG 还是扛得住的,别为了性能牺牲业务便利性。

场景二:C端信息流/评论区

类似于抖音、微博,用户只会无限下拉,数据量千万级甚至亿级。

结论: 必须使用游标分页。这是 Google、Stripe 等大厂 API 的标准设计规范。如果你在这里用 OFFSET,流量上来那一刻就是系统崩盘之时。

场景三:数据量大,但必须用 OFFSET

如果产品经理拿刀架在你脖子上非要“随机跳转”,怎么办?

救命稻草: 使用**“延迟关联”**(Late Row Lookup)。

先只查 ID 进行分页(走索引,快),拿到 10 个 ID 后,再回表去查具体数据。这能避免数据库在扫描丢弃那 100万行时,去读取庞大的数据文件,能救你一命。

最后的忠告

技术没有绝对的好坏,只有适不适合。

当你下周一坐在工位上时,不妨打开慢查询日志(Slow Query Log)看一眼。如果有大量的 OFFSET 语句耗时超过 1 秒,恭喜你,你抓到了一只潜在的性能“吞金兽”。

理解数据库的扫描代价,是后端工程师进阶的基本功,别让简单的分页成为你职业生涯的绊脚石。

最后留个悬念:

除了分页,你的索引可能也在“假装工作”。下期我们来聊聊《PostgreSQL 索引失效的十大离谱场景》,每一个都是血泪坑。


互动时刻:

你们公司的项目中,遇到过最奇葩的分页需求是什么?有没有人试过 OFFSET 一千万的?评论区晒出你的“惨痛经历”!