哪些分页方案可以处理快速变化的内容列表?

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&amp;page_num=2&amp;page_size=20)。因此,客户端保留请求下一页所需的所有状态,而服务器可以在分页方面保持无状态。

注意:如果用户刷新没有before 参数的URL(即http://some.server.com/animals),他们将看到新数据。如果用户使用 before 参数(即http://some.server.com/animals?before=2020-04-08T10:40:34.833Z&amp;page_num=2&amp;page_size=20)刷新 URL,他们将看到相同的数据。用户可以随时更改或删除before 参数以查看新数据。

【讨论】:

以上是关于哪些分页方案可以处理快速变化的内容列表?的主要内容,如果未能解决你的问题,请参考以下文章

Magnolia 中用于内容列表的分页表视图

php 分页查询怎么redis缓存

mybatis常用分页插件,快速分页处理

我如何告诉 ui-Bootstrap 分页哪些内容?

arcmap中的分级色彩表示内容对应的专题地图表示方法

分页的实现