在当今的数据驱动应用中,处理海量数据是后端开发面临的日常挑战。想象一下,如果你的电商网站有数百万条商品记录,试图在一个页面中加载所有数据不仅会导致浏览器崩溃,还会让数据库服务器不堪重负。这就是为什么分页成为现代应用中不可或缺的功能。
作为一名开发者,我们经常需要在 PostgreSQL 数据库中实现高效的数据分页。在这篇文章中,我们将深入探讨 PostgreSQL 中实现分页的不同方法,并结合 2026 年的工程实践,从最基础的 OFFSET 逐步深入到高性能的“键集分页”技术。我们将通过实际的代码示例,并结合现代 AI 辅助开发流程,帮助你理解每种方法的优缺点及适用场景。
什么是分页?为什么我们需要它?
简单来说,分页是一种将大型数据集分割成更小、更易于管理的“块”或“页”的技术。通过这种方式,用户每次只需要浏览一小部分数据(例如,每页显示 10 或 20 条记录)。
对于开发者而言,分页不仅是为了提升用户体验(UX),防止用户被海量信息淹没,更是为了系统性能的优化。通过限制单次查询返回的数据量,我们可以显著减少内存占用和网络传输延迟,确保应用在高并发下依然保持响应迅速。在我们最近的项目中,我们发现合理的分页策略甚至能将数据库的 CPU 负载降低 40% 以上。
在 PostgreSQL 中,我们有几种主要的方法来实现分页,每种方法都有其特定的使用场景和性能表现:
- 使用 INLINECODE01cc52e5 和 INLINECODE34d7d949(基础方法,适合简单场景)
- 使用键集分页 / 游标分页(高性能方法,2026 年的主流标准)
- 使用
FETCH FIRST(SQL 标准语法,更具可读性)
准备工作:构建测试环境
为了让我们更直观地理解这些分页方法的区别,我们需要一张包含足够数据的表来进行实验。让我们创建一个名为 INLINECODE2385dc94 的表,它包含 INLINECODE6cf11acc(主键)、INLINECODE084ad798(产品名称)、INLINECODE2719f1f0(价格)、INLINECODE16fd3d80(类别)以及 INLINECODE235e603a(时间戳)这几列。
图示:包含 ID、名称、价格和类别的 Products 表结构
在接下来的章节中,我们将基于这张表执行各种查询,看看不同分页技术是如何工作的。
目录
1. 使用 LIMIT 和 OFFSET:最直观(但并非最高效)的方法
当我们刚开始学习 SQL 时,INLINECODEbcd058e0 和 INLINECODE4cd1727c 通常是第一个接触到的分页手段。这种方法逻辑非常清晰:告诉数据库“给我跳过前 X 条记录,然后取接下来的 Y 条记录”。
基础语法与原理
INLINECODE85604af0 用于限制返回的行数,而 INLINECODEa408ba1f 用于指定在开始返回行之前要跳过的行数。
让我们看一个简单的例子。假设我们要获取第一页的数据,每页显示 5 条记录。因为是从头开始,所以偏移量为 0:
-- 获取第一页:前 5 条数据
SELECT id, name, price, category
FROM products
ORDER BY id
LIMIT 5 OFFSET 0;
输出:按 ID 升序排列的前 5 行数据。
代码解析:
在这个查询中,INLINECODEab0c2631 非常关键。如果没有明确的排序,数据库返回的顺序是不确定的,分页结果就会混乱。INLINECODEa97e1cc8 确保只返回 5 行,而 OFFSET 0 表示从第 0 行之后开始(也就是第 1 行)。
翻到下一页与深分页陷阱
当我们想看第二页时,逻辑上就是跳过前 5 条,再看接下来的 5 条:
-- 获取第二页:跳过前 5 条,取接下来的 5 条
SELECT id, name, price, category
FROM products
ORDER BY id
LIMIT 5 OFFSET 5;
虽然这在数据量小的时候运作完美,但在处理大数据量时,它有一个严重的性能陷阱。当你执行 OFFSET 10000 时,PostgreSQL 实际上必须扫描并丢弃前 10,000 行数据,然后才返回你想要的结果。随着页码的增加,查询速度会呈线性下降。在 2026 年,随着数据量的爆炸式增长,这种线性延迟是用户无法容忍的。
2. 使用键集分页(游标分页):2026 年的性能标准
为了解决 OFFSET 带来的性能问题,我们在现代生产环境中通常会转向键集分页,也常被称为游标分页。这不仅是大型科技公司的首选,也是我们构建高性能 API 的标准实践。
核心思想:记住“我们在哪了”
键集分页不再计算“跳过多少行”,而是记录“我们在哪里停下的”。我们将上一页最后一条记录的唯一标识(通常是 ID 或时间戳)作为下一页查询的起点(过滤器)。
实现代码
假设第一页我们已经获取到了 ID 为 1 到 5 的数据。现在我们要获取下一页。我们不需要跳过 5 行,而是直接告诉数据库:“请给我 ID 大于 5 的接下来的 5 条数据”。
-- 键集分页示例:获取 ID 大于 5 的下 5 条记录
-- 这里的 5 就是上一页最后一条记录的 ID(我们称之为“游标”)
SELECT id, name, price, category
FROM products
WHERE id > 5
ORDER BY id
LIMIT 5;
!键集分页示例.png)
输出:ID 从 6 到 10 的产品详情。
为什么它更快?
在这个查询中,WHERE id > 5 让数据库能够直接利用 B-Tree 索引进行“定位”。数据库根本不需要读取前 5 行数据,而是直接跳到 ID 为 6 的位置开始读取。这种方法的查询时间复杂度是常数级别的 O(1),无论你翻到第 100 页还是第 10000 页,查询速度都一样快。这就是为什么它非常适合处理千万级甚至亿级数据。
3. 处理复杂排序:解决非唯一键的痛点
在实际的业务场景中,我们很少只按 ID 排序。更多的场景是按“价格”、“评分”或“创建时间”排序。这里有一个巨大的坑:非唯一排序字段会导致数据错乱。
如果你仅仅按照 INLINECODEd8d76f74(价格)排序,而很多产品的价格都是一样的,那么 INLINECODE94c693b3 可能会漏掉某些价格同样是 100 但 ID 不同(且未显示)的产品。
最佳实践:组合键游标
为了确保顺序的唯一性,我们必须在 ORDER BY 中包含一个唯一列(通常是主键 ID)作为“决胜局”。
-- 生产环境示例:组合排序与分页
-- 假设上一页最后一条记录是 price=100, id=5
-- 我们需要查找所有“价格小于 100”的记录,或者“价格等于 100 但 ID 小于 5”的记录
SELECT id, name, price, category
FROM products
WHERE
price < 100 -- 主要排序条件
OR (price = 100 AND id < 5) -- 处理价格相同的情况,确保连续性
ORDER BY price DESC, id DESC -- 组合排序,保证顺序唯一
LIMIT 5;
这种写法虽然在 SQL 看起来有点冗长,但它能保证数据分页的绝对准确性,不会出现任何数据重复或丢失。这是我们在处理复杂列表时必须严格遵守的原则。
4. 前沿探索:基于时间戳的实时流式分页
随着社交媒体和实时协作应用(如 Figma, Notion 类应用)的普及,传统的分页正在向“实时流”演变。在 2026 年,我们越来越多地使用基于时间戳的分页来处理动态插入的数据。
想象一下,你正在构建一个类似 Twitter 的信息流。用户不断发布新内容,如果使用 INLINECODE70f69e47,用户每次刷新看到的第一条数据都会变,这会导致体验非常割裂。而基于 INLINECODE93529193(创建时间)的键集分页能完美解决这个问题。
-- 时间戳分页示例:实现“加载更多”功能
-- last_seen_time 是上一页最后一条记录的时间戳
-- last_seen_id 用于处理同一毫秒内的并发插入
SELECT id, content, created_at
FROM posts
WHERE
(created_at, id) < ('2026-05-20 10:00:00', 12345)
ORDER BY created_at DESC, id DESC
LIMIT 20;
这种方法的优势在于:即使数据库中插入了新数据,你的“下一页”查询结果依然是稳定的,新数据只会在用户刷新第一页时出现,而不会打乱用户正在浏览的历史记录。
5. 2026 开发新范式:AI 辅助与 GraphQL 集成
作为现代开发者,我们不仅需要写 SQL,还需要思考如何将这些技术更好地集成到前沿的开发工作流中。
GraphQL 中的游标分页
在 2026 年,GraphQL 已成为 API 设计的主流标准。GraphQL 规范中的 Relay Cursor Connections 规范本质上就是我们在上面讨论的键集分页。GraphQL 要求返回一个 cursor(通常是 Base64 编码的查询参数),而不是页码。
// 前端请求示例 (GraphQL)
query {
products(first: 10, after: "eyJpZCI6NX0=") {
edges {
node {
name
price
}
cursor // 获取这个游标用于下一次请求
}
pageInfo {
hasNextPage
endCursor
}
}
}
这种模式使得前端开发者不需要关心具体的分页逻辑,只需传递游标,大大简化了客户端代码的复杂度。
利用 AI (Cursor/Copilot) 优化查询
在我们团队的实际工作中,我们利用现代 AI 编程工具(如 Cursor 或 GitHub Copilot)来辅助编写复杂的分页查询。例如,当我们需要处理一个包含多列排序的复杂分页时,我们可以直接提示 AI:
/Prompt: Generate a PostgreSQL keyset pagination query for ‘orders‘ table,
ordered by ‘status‘ ASC and ‘created_at‘ DESC. Ensure we handle the cursor
logic correctly to avoid row skips. The page size is 50.
AI 不仅会生成 SQL,还能帮我们检测潜在的索引缺失问题。这正是 2026 年“AI 原生开发”的体现——我们不再从零手写 boilerplate 代码,而是专注于业务逻辑和数据结构的设计,让 AI 帮我们处理繁琐的语法细节和边界情况。
常见陷阱与排查
- 数据重复或遗漏:如果在两次请求之间数据库有新数据插入,基于
OFFSET的分页可能会导致用户看到重复的数据(因为新数据挤进来了)。
解决方案*:键集分页天然解决了这个问题,因为它基于绝对的数据位置而非相对偏移。
- 无法跳转到任意页:键集分页非常适合“下一页”或“加载更多”,但很难直接跳转到“第 300 页”。
解决方案*:如果你的业务必须有“页码”导航(例如后台管理列表),传统 OFFSET 可能是唯一选择,或者你需要维护一个独立的搜索服务(如 Elasticsearch)来支持任意跳转。
- 深度分页的性能崩溃:当你看到查询计划中出现“Bitmap Heap Scan”且耗时巨大时,通常是因为
OFFSET太大。
解决方案*:立即重写为键集分页查询,并确保在 INLINECODE39827c64 和 INLINECODE37a09ef5 涉及的列上建立了联合索引。
总结:你应该选择哪种方法?
在这篇文章中,我们探索了 PostgreSQL 中实现分页的不同视角。作为开发者,我们需要根据具体的业务场景做出权衡:
- LIMIT 和 OFFSET:最适合内部后台管理系统、数据量较小且需要支持任意页码跳转的场景。开发简单,但在大数据量下有性能隐患。
- 键集分页(游标分页):这是现代 Web 应用的首选。它提供了恒定的查询性能,完美支持“无限滚动”和移动端数据流。配合 GraphQL 使用效果更佳。
- 以键为中心的复杂分页:处理多字段排序、时间戳流式数据的利器。虽然写起来稍微复杂一点,但能带来极佳的用户体验和系统稳定性。
给开发者的建议:
下一次当你需要编写分页逻辑时,不妨先问自己:“我的用户是像刷 Instagram 一样顺序浏览,还是像查 Excel 一样随意跳转?”。如果是前者,请毫不犹豫地选择键集分页。并且,尝试让 AI 帮你生成初步的代码框架,你则专注于审查索引策略和业务逻辑的正确性。
希望这篇文章能帮助你更好地理解 PostgreSQL 的分页机制,并能在 2026 年的技术栈中构建出更高效的应用。