哪些分页方案可以处理快速变化的内容列表?
Posted
技术标签:
【中文标题】哪些分页方案可以处理快速变化的内容列表?【英文标题】:What pagination schemes can handle rapidly-changing content lists? 【发布时间】:2012-03-25 00:35:30 【问题描述】:当您的内容排名可以快速变化时,分页会很困难,而当每个用户的排名不同时,分页就更难了。 (让我们将无限滚动视为一种链接不可见的分页。)有两个难题:顶部的新增内容和重新排列的内容。
让我们忘记新添加的内容,并接受您必须刷新第 1 页才能看到它。让我们也假设我们正在做纯ORDER BY position
;如果您通过其他方式订购,则可能必须使用窗口功能。我们的页面每页有 4 行动物。他们开始了:
+----+----------+-----------+
| id | position^| animal |
+----+----------+-----------+
| 1 | 1 | Alpacas |
| 2 | 2 | Bats |
| 3 | 3 | Cows |
| 4 | 4 | Dogs |
| 5 | 5 | Elephants |
| 6 | 6 | Foxes |
| 7 | 7 | Giraffes |
| 8 | 8 | Horses |
+----+----------+-----------+
在我们获取第 1 页之后,在我们获取第 2 页之前,很多项目会四处移动。数据库现在是:
+----+----------+-----------+
| id | position^| animal |
+----+----------+-----------+
| 4 | 1 | Dogs |
| 2 | 2 | Bats |
| 1 | 3 | Alpacas |
| 5 | 4 | Elephants |
| 6 | 5 | Foxes |
| 7 | 6 | Giraffes |
| 3 | 7 | Cows |
| 8 | 8 | Horses |
+----+----------+-----------+
常见的方法有以下三种:
偏移/限制方法
这是典型的幼稚方法;在 Rails 中,will_paginate 和 Kaminari 是这样工作的。如果我想获取第 2 页,我会这样做
SELECT * FROM animals
ORDER BY animals.position
OFFSET ((:page_num - 1) * :page_size)
LIMIT :page_size;
获取第 5-8 行。我永远不会看到大象,我会看到两次奶牛。
最后一次看到的 ID 方法
Reddit 采用了不同的方法。客户端不会根据页面大小计算第一行,而是跟踪您看到的最后一个项目的 ID,例如书签。当您点击“下一步”时,他们会从该书签开始查找:
SELECT * FROM animals
WHERE position > (
SELECT position FROM animals
WHERE id = :last_seen_id
)
ORDER BY position
LIMIT :page_size;
在某些情况下,这比页面/偏移效果更好。但在我们的案例中,最后一次看到的帖子 Dogs 向右放大到 #1。所以客户发了?last_seen_id=4
,我的第2页是蝙蝠、羊驼、大象和狐狸。我没有错过任何动物,但我见过两次蝙蝠和羊驼。
服务器端状态
HackerNews(以及我们的网站,现在)通过服务器端的延续解决了这个问题;他们为您存储整个结果集(或至少提前几页?),并且“更多”链接引用该延续。当我获取第 2 页时,我要求“原始查询的第 2 页”。它使用相同的偏移量/限制计算,但由于它与原始查询相反,我根本不在乎事情现在已经移动了。我看到大象、狐狸、长颈鹿和马。没有重复,没有遗漏的项目。
缺点是我们必须在服务器上存储大量状态。在 HN 上,它存储在 RAM 中,实际上这些延续通常在您按下“更多”按钮之前就过期,迫使您一直返回第 1 页以找到有效链接。在大多数应用程序中,您可以将其存储在 memcached 中,甚至可以存储在数据库本身中(使用您自己的表,或者在 Oracle 或 PostgreSQL 中,使用可保持游标)。根据您的应用程序,可能会影响性能;至少在 PostgreSQL 中,您必须找到一种方法来再次访问正确的数据库连接,这需要大量的粘性状态或一些巧妙的后端路由。
只有这三种可能的方法吗?如果没有,是否有计算机科学概念可以让我在谷歌上阅读相关内容?有没有办法在不存储整个结果集的情况下近似延续方法?从长远来看,存在复杂的事件流/时间点系统,其中“在我获取第 1 页时的结果集”是永远可推导出的。还不够……?
【问题讨论】:
我建议从不同的角度来看待它。也许完全可以避免分页——只需使用无限滚动 + 一些扩展脚本即可更新列表而无需重新加载页面并显示适当的 ↑/↓ 符号以方便用户使用。不过,这取决于您的用例。更新:FWIW,这是来自 UX StackExchange 的 a related question。 是的,这不适用于我们的用例......事情会不断重新排列,您不希望显示不断更新。好主意,不过。 您可以在客户端存储状态,并发送所有看到记录的ID。 感谢您在问题中提供答案。 【参考方案1】:Oracle 很好地处理了这个问题。只要游标处于打开状态,您就可以根据需要多次获取,并且您的结果将始终反映打开游标的时间点。它使用撤消日志中的数据来虚拟回滚游标打开后提交的更改。
只要所需的回滚数据仍然可用,它就会工作。最终日志被回收,回滚数据不再可用,因此有一些限制,具体取决于日志空间、系统活动等。
不幸的是(IMO),我不知道有任何其他数据库可以像这样工作。我使用过的其他数据库使用锁来确保读取一致性,如果您希望在很短的时间内保持读取一致性,这将是一个问题。
【讨论】:
原来 PostgreSQL 也有可保持的游标。在 Oracle 上,您可以从不同的连接、从属设备等处点击该光标吗? PostgreSQL 可保持游标是基于磁盘的(因此您不会占用 RAM)并且它们也可以处理事务日志,但它们只能在同一连接上使用,因此您必须确保粘性或进行一些后端路由.【参考方案2】:我们现在使用服务器端状态方法,在第一个查询中缓存整个结果,因此我们总是返回一个一致的列表。只要我们的查询已经返回所有行,这将起作用;最终我们将需要使用最近邻方法,但这是行不通的。
但我认为还有第四种可能性,它的扩展性非常好,只要:
-
您不需要保证没有重复,只需要很高的可能性
您可以在滚动期间丢失一些内容,只要避免重复即可
该解决方案是“最后一次看到的 ID”解决方案的一种变体:让客户保留 5 个、10 个或 20 个书签,而不是保留 5 个或 10 个或 20 个书签 - 数量少到可以有效存储它们。查询最终看起来像:
SELECT * FROM posts
WHERE id > :bookmark_1
AND id > :bookmark_2
...
ORDER BY id
随着书签数量的增加,您 (a) 从某个时间点开始超过所有 n 个书签但 (b) 仍然看到重复内容的可能性会迅速降低,因为它们都已重新排序。
如果将来有漏洞或更好的答案,我很乐意不接受这个答案。
【讨论】:
如果你的 id 是 UUID,你会在这里做什么【参考方案3】:解决方案 1:“hacky 解决方案”
解决方案可能包括您的客户跟踪已看到的内容,例如 ID 列表。每次您需要另一个页面时,将此 ID 列表添加到服务器调用的参数中。然后,您的服务器可以对内容进行排序、删除已经看到的内容并应用偏移量以获得正确的页面。
我不会推荐它,我坚持 hacky。我只是在这里写下来,因为它很快并且可以满足一些需求。以下是我能想到的坏事:
1) 它需要在客户端进行一些工作才能使其正确(我上面的句子中的“已经看到”是什么意思,如果我转到上一页怎么办?)
2) 生成的订单不反映您的真实订购政策。尽管政策应该将内容放在第 1 页,但内容可能会显示在第 2 页。这可能会导致用户误解。让我们以堆栈溢出及其以前的排序策略为例,这意味着首先获得最多支持的答案。我们可能会在第 2 页有一个有 6 个赞成的问题,而在第 1 页有一个有 4 个赞成的问题。当用户仍在第 1 页时发生 2 个或更多赞成票时,就会发生这种情况。--> 可能会让用户感到惊讶.
解决方案 2:“客户端解决方案”
它基本上是您所谓的“服务器端状态”的客户端等效解决方案。只有在服务器端跟踪完整订单不够方便时,它才有用。如果项目列表不是无限的,它就可以工作。
调用您的服务器以获取完整(有限)订单列表 + 项目/页数 保存在客户端 直接通过内容的 ID 检索项目。【讨论】:
【参考方案4】:聚会很晚,但我们尝试了以下方法。我们使用的是连续加载,而不是用户在页面之间来回切换。
客户端会构建一个它所显示的所有 ID 的列表,因此在第一次设置之后它可能是: 4,7,19,2,1,72,3
当我们加载更多内容时,我们会使用相同的排序执行相同的查询,但会添加以下内容: WHERE id NOT IN (4,7,19,2,1,72,3)
NOT IN 列表可以很快增长。对我们来说,这不是问题,因为我们的内部工具通常不会产生大量结果。
我想添加另一个想法。也许可以对此应用服务器端添加。当用户搜索时,将他们获得的所有 ID 添加到带有搜索链接的表中。当客户端想要更多时,它只需要提供搜索 ID(或使用服务器端状态),查询就可以加入他们的搜索数据。
【讨论】:
【参考方案5】:如果行包含创建时间戳,则查询可以包含“之前”过滤器。这确保不包括在时间戳之后创建的任何行,因此分页是一致的(假设行在常量列上排序)。下面是一个示例 SQL 查询,它假定 animals.position
列中的值是常量。
SELECT
a.*
FROM
animals a
WHERE
a.creation < :before
ORDER BY
a.position
OFFSET ((:page_num - 1) * :page_size)
LIMIT :page_size
当客户端发出初始请求时(例如http://some.server.com/animals
),服务器将:before
设置为当前时间,:page_num
设置为 1,:page_size
设置为 20。服务器的响应包括一个请求链接下一页设置了所有 3 个参数(例如 http://some.server.com/animals?before=2020-04-08T10:40:34.833Z&page_num=2&page_size=20
)。因此,客户端保留请求下一页所需的所有状态,而服务器可以在分页方面保持无状态。
注意:如果用户刷新没有before
参数的URL(即http://some.server.com/animals
),他们将看到新数据。如果用户使用 before
参数(即http://some.server.com/animals?before=2020-04-08T10:40:34.833Z&page_num=2&page_size=20
)刷新 URL,他们将看到相同的数据。用户可以随时更改或删除before
参数以查看新数据。
【讨论】:
以上是关于哪些分页方案可以处理快速变化的内容列表?的主要内容,如果未能解决你的问题,请参考以下文章