添加分页前,先定义唯一的排序顺序

使用 AI 辅助编写与复核。

本文目录

请智能体添加分页之前,先明确所有行的完整顺序,以及翻页之间数据发生变化时应如何处理。只用时间戳排序,可能无法确定时间相同的行谁先谁后。唯一排序能解决这些并列情况,但不会让不同请求之间的数据集保持不变。

因此,一份实用的分页需求要明确两项规则:行怎样排序,以及读者是在继续浏览一份保留的结果,还是每次请求都看到实时变化。随后给出完整的分页示例,让实现者可以逐项核对。

下面的虚构列表包含八条记录。日期和 ID 都是为示例编写的数据,并非应用日志。浏览时,我们会保留一份结果快照;只有明确选择重新开始后,才看到一条新增和一条删除带来的变化。

为并列的行定义完整顺序

初始数据如下,特意没有按顺序排列:

整数 ID created_at,UTC
104 2026-01-15T09:30:00Z
101 2026-01-15T10:00:00Z
107 2026-01-15T09:00:00Z
103 2026-01-15T09:30:00Z
106 2026-01-15T09:00:00Z
102 2026-01-15T10:00:00Z
108 2026-01-15T08:45:00Z
105 2026-01-15T09:15:00Z

本例选择以下规则:

  • 按 created_at 从新到旧排序。
  • 时间戳相同时,按唯一的整数 id 升序排列。
  • 两个字段都必须存在。时间戳缺失或 ID 重复,在本例结果集中都属于无效输入,不能悄悄为其指定顺序。
  • 保持字段含义一致:时间戳是 UTC 创建时间;ID 是整数,不能把它当成格式化后的字符串,按显示文本排序。

完整的预期顺序是:

101, 102, 103, 104, 105, 106, 107, 108

在 PostgreSQL 18 中,排序部分可以写成:

ORDER BY created_at DESC, id ASC

PostgreSQL 的 18 版 LIMIT/OFFSET 文档解释了为何截取部分结果需要可预测且唯一的排序。这支持明确并列时的排序依据,但没有提供本应用的快照规则。本文没有执行任何 PostgreSQL 查询。

明确浏览期间哪些内容保持不变

这个虚构列表选择保留结果的规则。打开列表时,捕获快照 S1,包括哪些行属于结果、各行的值、筛选条件和排序。后续分页一直使用这份结果,直到读者选择“重新开始”。

这是需要实现的需求,不能只加上 ORDER BY 就自动获得。实现者必须确认,所选应用如何保留并识别这份结果。如果做不到,就应明确提出使用实时数据的替代方案,不能把实时页面称为快照。

每页 3 条时,S1 的全部三页如下:

S1 中的页码 预期 ID 此页之后还有行吗?
1 101, 102, 103 有
2 104, 105, 106 有
3 107, 108 没有

把这些页面中的 ID 列表依次拼接,就会得到完整的八个 ID,每个恰好出现一次。最后一页较短,因为只剩两行。越过末尾的请求应返回空页,且没有后续可继续的位置;界面不应再提供“下一页”操作。

完全为空的快照,其第一页也为空,并且没有后续页面。界面应显示无结果状态,不能让人误以为第 1 页仍在加载。

把旧快照与新结果分开

现在假设读者看完第 1 页后发生以下变化:

  1. 插入 ID 为 109 的记录,创建时间为 2026-01-15T10:15:00Z。
  2. 从当前底层数据集中删除 ID 为 104 的记录。

按照选定规则,继续浏览 S1 时,第 2 页仍然返回 104, 105, 106。保留的结果中包含历史行 104。它的出现反映的是 S1,并不能证明当前数据集仍有该行。本文规定的是结果列表的显示方式,不涉及打开一条现已删除的当前记录时应如何处理。

只要 S1 仍可用,重新打开 S1 的第 2 页就应得到相同内容。如果 S1 已不可用,应明确要求重新开始。不能沿用旧快照的标识,悄悄换成更新的结果。

选择“重新开始”后,从变化后的数据集中捕获 S2。完整顺序为:

109, 101, 102, 103, 105, 106, 107, 108

新的三页顺序是:

S2 中的页码 预期 ID 此页之后还有行吗?
1 109, 101, 102 有
2 103, 105, 106 有
3 107, 108 没有

这是两份各自完整、彼此独立的结果。把 S1 的第 1 页与 S2 的第 2、3 页混在一起,不属于其中任何一份快照。

看清实时偏移分页如何跨越结果边界

考虑另一项规则:每次分页请求都读取当前数据。如果读者先看过 S1 的第一页,然后在变化后的数据集上请求偏移量 3,第二次请求会返回:

103, 105, 106

即使排序唯一,ID 103 仍会再次出现。因为应用偏移量之前,输入集合已经变化。唯一的并列排序依据解决了每份结果内部的顺序,却没有让两次请求包含相同的成员。

这个反例并不意味着应禁止使用偏移量。对同一份保留结果使用偏移量,就能得到前文的页面切片。游标也可以表示某个排序键之后的位置,但仍需明确它与实时新增、删除以及快照标识之间的关系。

设计问题 偏移量式位置 游标式位置
怎样描述下一个位置? 要跳过的行数 由应用定义的位置,通常与排序键有关
是否不再需要完整排序? 否 否
仅凭位置本身,能否固定数据集? 否 否
需求中还应单独确定什么? 使用实时结果还是保留结果、每页条数变化、重复打开页面的行为 同样的规则,以及游标究竟标识什么

先确定这些用户可见的规则,再选择实现方式。这个小例子没有比较数据库性能,也没有宣称任何一种位置方案普遍更好。

明确每页条数变化时的行为

本例中,改变每页条数会回到同一份保留快照的第一页。它不会捕获新快照,也不会重新解释旧页码。

S1 每页 4 条时:

页码 预期 ID
1 101, 102, 103, 104
2 105, 106, 107, 108

第 2 页之后没有更多行。这个规则让每页条数变化的结果可以核对,不必让实现者猜测,究竟应保留原来的行位置还是页码。

复制这份分页需求

为所附八行虚构数据集规划分页。

排序:
created_at 降序,再按唯一的整数 id 升序排列。
两个字段都必填。时间戳相同时,不要依赖输入顺序,
也不要依赖数据库偶然给出的顺序。

浏览规则:
打开结果时,捕获 S1 的成员、值、筛选条件和顺序。
翻页时继续使用同一份保留结果。明确选择“重新开始”后,
才把当前数据集捕获为新结果。如果保留结果不可用,
应要求重新开始,不能悄悄用新数据替换。

S1 每页 3 条的预期页面:
[101,102,103]、[104,105,106]、[107,108]。
第 1 页之后,在 2026-01-15T10:15:00Z 插入 109,并删除当前行 104。
继续浏览或重新打开 S1 第 2 页,仍应得到 [104,105,106]。

重新开始后,S2 的预期页面:
[109,101,102]、[103,105,106]、[107,108]。
不要混用不同结果的页面切片。

把每页条数改成 4,会回到同一快照的第一页。
此时 S1 为 [101,102,103,104]、[105,106,107,108]。
空结果和越过末尾的请求都没有可继续的下一页。

检查应用当前的排序、分页位置和结果保留机制。
根据这些需求建议如何处理偏移量或游标;
解释怎样确定结果标识及其可用性。
如果不支持保留结果,提出改动前先指出这一缺口。

返回范围明确的计划,以及对并列值、全部三页、每页条数变化、
空结果/末尾、新增/删除和重新打开页面的检查。
把跳转到当前记录与显示历史结果行的行为分开。
只使用本地案例数据。本步骤不得连接数据库、调用真实 API、
修改生产数据、安装软件包或部署。

配套的 Python 内存计算在 Python 3.12.14 上复现了两份完整顺序、各自的页面切片、每页条数变化示例,以及实时偏移分页中的重复项。这核对的是这些编写示例的计算结果;快照保留、数据库行为和界面渲染仍需要针对实际实现的证据。