PostgreSQL分页踩坑!OFFSET越用越卡?换种写法秒提速

12333社保查询网www.sz12333.net.cn 2026-02-15来源:人力资源和社会保障局

一、明明没改代码,分页怎么突然变慢了?

  做后端开发的朋友,几乎没人没写过OFFSET分页。产品刚上线时,数据量少,分页加载秒开,大家都觉得这写法简单又好用,堪称SQL分页“万能模板”。

  可没人能想到,这个看似无害的写法,会在数据量暴涨后,悄悄拖垮整个系统——页面加载从几百毫秒变成十几秒,数据库CPU直接拉满,用户投诉不断,程序员排查半天,才发现罪魁祸首竟是一句简单的“LIMIT … OFFSET …”。

  你是不是也遇到过这种困境?明明代码没动、服务器没升级,分页却越用越卡?其实不是PostgreSQL不行,也不是你技术不到位,而是你用错了分页方式,踩中了PostgreSQL最隐蔽的性能陷阱。

  先给大家说个真实场景:某创业公司做用户管理系统,初期用户只有几千,用OFFSET分页毫无压力;可半年后用户破100万,分页翻到第100页时,页面直接加载超时,数据库日志里全是OFFSET相关的慢查询,排查了3天,才找到问题根源。

  关键技术说明:本文核心围绕PostgreSQL数据库的分页机制展开,PostgreSQL是开源免费的关系型数据库,由PostgreSQL全球开发组维护,GitHub星标高达20.8万+,广泛应用于后端开发、大数据存储等场景,因其稳定性强、兼容性好,成为很多企业的首选数据库,而OFFSET分页则是PostgreSQL中最常用、也最容易踩坑的分页方式。

二、核心拆解:OFFSET为啥会拖慢PostgreSQL?附替代方案实操

  很多程序员之所以偏爱OFFSET分页,核心就是简单——不用复杂逻辑,只要用OFFSET跳过前面的行,再用LIMIT取固定数量的内容,就能实现分页,几乎所有SQL教程都会教这种写法。

1. OFFSET分页的“假象”:看似跳过,实则白干

  我们先看一段最常见的OFFSET分页SQL,也是大家平时写得最多的代码:

  SELECT id, username, email, created_atFROM usersORDER BY created_at DESCLIMIT 20 OFFSET 1000;

  这段代码的逻辑很简单:取用户表中,按创建时间倒序排列,跳过前1000行,取后面20行(也就是第51页,每页20条)。

  看起来没毛病,但PostgreSQL的执行逻辑,和我们想的完全不一样——它根本不会“跳过”前1000行,而是会先把前1001行全部查出来,构建一个完整的结果集,再把前1000行扔掉,只返回最后20行。

  这就意味着,你翻的页越靠后,PostgreSQL做的无用功就越多:

  翻第1页:查20行,返回20行,效率拉满;

  翻第10页:查200行,扔180行,效率尚可;

  翻第100页:查2000行,扔1980行,开始卡顿;

  翻第500页:查10000行,扔9980行,数据库CPU直接飙升,页面加载超时。

  更致命的是,这种问题在开发、测试环境根本发现不了——测试环境数据量少,哪怕翻第100页,也只是查几千行,感觉不到卡顿;可一旦上线,数据量突破10万、100万,问题就会瞬间爆发。

2. 替代方案:Keyset分页(游标分页),翻多少页都不卡

  既然OFFSET分页在大数据量下会翻车,那有没有既能满足分页需求,又能保证性能的写法?答案是肯定的——Keyset分页(也叫游标分页)。

  和OFFSET分页的“跳过多少行”不同,Keyset分页的核心逻辑是“从某个位置开始取后面的行”,就像我们看书用书签,不用数前面翻了多少页,只要从书签所在的位置继续看就行,效率大大提升。

第一步:理解Keyset分页的核心逻辑

  OFFSET分页问的是“给我第51页的20行”,而Keyset分页问的是“给我某个位置之后的20行”。它不需要计算跳过多少行,只需要记住上一页最后一行的两个关键值(游标),下一页就从这个游标之后开始查询。

第二步:实操步骤(附完整代码)

  我们还是以用户表为例,一步步实现Keyset分页,所有代码可直接复制到项目中使用。

  1. 初始查询(第一页,无游标):

  SELECT id, username, email, created_atFROM usersORDER BY created_at DESC, id DESCLIMIT 20;

  这里有个关键细节:排序时,除了按created_at(创建时间)倒序,还要加上id(主键)倒序。原因很简单,避免出现创建时间相同的行,导致分页时数据错乱、重复。

  2. 获取游标(上一页最后一行的关键值):

  执行完第一页的查询后,我们需要记住最后一行的created_at和id两个值,这两个值就是我们的“游标”。比如,第一页最后一行的created_at是2025-01-15 10:23:45,id是8432,那么游标就是这两个值的组合。

  3. 下一页查询(根据游标查询):

  SELECT id, username, email, created_atFROM usersWHERE (created_at, id) < ('2025-01-15 10:23:45', 8432)ORDER BY created_at DESC, id DESCLIMIT 20;

  这里的逻辑是:查询所有“创建时间小于2025-01-15 10:23:45”,或者“创建时间相等但id小于8432”的用户,按同样的顺序排列,取前20行——这就是第二页的内容。

  4. 关键优化:创建对应索引

  Keyset分页的性能,完全依赖于索引——如果没有对应的索引,PostgreSQL还是会全表扫描,效率和OFFSET分页没区别。因此,必须创建和排序顺序一致的索引:

  CREATE INDEX idx_users_created_at_idON users (created_at DESC, id DESC);

  注意:索引的列顺序必须和查询的排序顺序完全一致(先created_at DESC,再id DESC),差一点都不行,否则索引不会生效。

第三步:Node.js实战示例(可直接复用)

  下面给大家一个完整的Node.js实战代码,封装了Keyset分页的逻辑,传入游标就能查询对应页的内容,无需手动计算OFFSET,直接复制到项目中即可使用:

  async function getUsers(cursor = null, limit = 20) { let sql = ` SELECT id, username, email, created_at FROM users `; const params = []; if (cursor) { const [createdAt, id] = cursor.split('_'); sql += ` WHERE (created_at, id) < ($1, $2)`; params.push(createdAt, id); } sql += ` ORDER BY created_at DESC, id DESC LIMIT ${params.length + 1} `; params.push(limit); const { rows } = await pool.query(sql, params); // 生成下一页的游标 const nextCursor = rows.length `${rows[rows.length - 1].created_at}_${rows[rows.length - 1].id}` : null; return { data: rows, nextCursor };}

  代码逻辑很简单:如果传入了游标,就按游标过滤数据;如果没有传入游标(第一页),就查询前20行;最后返回当前页数据和下一页的游标,前端拿到游标后,下次请求传入即可获取下一页内容。

三、辩证分析:Keyset分页虽好,却不是万能的

  看到这里,很多朋友可能会觉得,Keyset分页这么好用,直接替换掉所有OFFSET分页就行了。但事实上,没有任何一种技术是完美的,Keyset分页有它的优势,也有它的局限性,盲目替换反而会适得其反。

  先肯定Keyset分页的核心价值:它最大的优势就是“稳定”,无论你翻到第10页还是第1000页,查询效率几乎一致,不会出现越翻越卡的情况,而且无需做复杂的OFFSET计算,减少了代码出错的概率,非常适合大数据量的生产环境。

  但我们也要清醒地认识到它的局限性——在某些场景下,Keyset分页反而不如OFFSET分页好用:

  1. 无法直接跳转到任意页码:Keyset分页只能“上一页、下一页”,无法像OFFSET分页那样,直接跳转到第50页、第100页。如果你的产品需求是“用户可以自由输入页码跳转”(比如后台管理系统的分页),那么Keyset分页就不适用。

  2. 不适合需要精确总页数的场景:Keyset分页无法直接获取总页数,只能通过“是否有下一页游标”来判断是否到底。如果你的产品需要显示“共100页,当前第5页”,那么Keyset分页就需要额外查询总条数,反而增加了复杂度。

  3. 对排序字段有要求:Keyset分页依赖于排序字段的唯一性,如果没有合适的排序字段(比如没有主键、没有唯一时间戳),就无法实现Keyset分页,只能用OFFSET分页。

  这就引发了一个值得所有程序员思考的问题:我们写代码,到底是追求“技术先进”,还是追求“贴合需求”?其实,最好的方案从来不是“非此即彼”,而是“因地制宜”——根据自己的业务场景,选择最适合的分页方式。

四、现实意义:选对分页方式,省下一台服务器的钱

  很多程序员觉得,分页变慢了,大不了升级服务器、增加数据库节点,花点钱就能解决。但事实上,大多数时候,分页变慢不是硬件不行,而是我们的写法不对——选对分页方式,不仅能解决卡顿问题,还能省下大量的服务器成本,甚至减少运维压力。

  我见过很多创业公司,因为分页写法不当,导致数据库CPU长期高负载,不得不额外购买服务器、升级数据库配置,每月多花几千上万元的成本;而仅仅是把OFFSET分页换成Keyset分页,再加上合适的索引,数据库负载就直接下降了60%以上,无需升级硬件,就能解决卡顿问题。

  对于大厂来说,分页优化的价值更是不可估量。比如某电商平台,用户列表分页每天有上亿次请求,如果用OFFSET分页,数据库根本扛不住,必须用Keyset分页才能保证稳定性;而优化后,不仅减少了数据库节点的数量,还降低了用户投诉率,提升了用户留存。

  除此之外,选对分页方式,还能提升我们的开发效率和代码质量:

  1. 减少调试成本:OFFSET分页的卡顿问题,在开发环境很难发现,往往要等到上线后才会暴露,排查起来耗时耗力;而Keyset分页的性能稳定,开发阶段就能验证,减少了线上bug的概率。

  2. 降低维护成本:Keyset分页的逻辑更简洁,无需手动计算OFFSET,减少了代码中的“魔法数字”,后续维护、迭代也更方便,新人接手也能快速上手。

  其实,PostgreSQL的分页陷阱,从来都不是数据库本身的问题,而是我们对技术的理解不够深入——我们习惯于用最简单、最熟悉的方式写代码,却忽略了场景的适配性,最终导致线上问题频发。这也提醒我们:作为程序员,不仅要会写代码,还要懂原理、会优化,才能写出高效、稳定的代码。

五、互动话题:你踩过OFFSET分页的坑吗?

  看到这里,相信很多后端程序员都会深有共鸣——谁还没因为OFFSET分页踩过坑呢?

  或许你曾经因为分页卡顿,熬夜排查到凌晨;或许你已经发现了OFFSET的问题,换成了Keyset分页,收获了性能的提升;或许你还有更好的分页优化技巧,想和大家分享。

  评论区留下你的经历和看法:你平时用哪种分页方式?有没有踩过OFFSET分页的坑?Keyset分页你是怎么实操的?

  关注我,每天分享PostgreSQL优化技巧、后端实战干货,帮你避开开发中的那些坑,提升代码效率,少熬夜、多摸鱼!

本文标题:PostgreSQL分页踩坑!OFFSET越用越卡?换种写法秒提速本文网址:https://www.sz12333.net.cn/zhzx/kexue/72682.html 编辑:12333社保查询网

本站是社保查询公益性网站链接,数据来自各地人力资源和社会保障局,具体内容以官网为准。
定期更新查询链接数据 苏ICP备17010502号-11