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

时间:2012-03-07 13:15:23

标签: database pagination complex-event-processing

当您的内容排名可以快速变化时分页很难,而且当这些排名因用户而异时更难。 (让我们将无限卷轴视为一种链接不可见的分页。)有两个难题:顶部新添加的内容和重新排列的内容。

让我们忘记新添加的内容,并接受您必须刷新第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页之前,很多项目都会移动。 DB现在是:

+----+----------+-----------+
| 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_paginateKaminari的工作方式。如果我想获取第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页是Bats,Alpacas,Elephants和Foxes。我没有错过任何动物,但我两次看到了Bats和Alpacas。

服务器端状态

HackerNews(以及我们的网站,现在)通过服务器端延续来解决这个问题;他们为您存储整个结果集(或至少提前几页?),“更多”链接引用该延续。当我获取第2页时,我要求“我的原始查询的第2页”。它使用相同的偏移/限制计算,但由于它与原始查询相反,我根本不关心事情现在已经移动了。我看到大象,狐狸,长颈鹿和马。没有重复,没有遗漏物品。

缺点是我们必须在服务器上存储很多状态。在HN上,它存储在RAM中,实际上这些延续通常会在您按“更多”按钮之前到期,迫使您一直返回到第1页以查找有效链接。在大多数应用程序中,您可以将其存储在memcached中,甚至存储在数据库本身中(使用您自己的表,或者使用可保持的游标在Oracle或PostgreSQL中)。根据您的应用程序,可能会有性能损失;至少在PostgreSQL中,你必须找到一种方法再次点击正确的数据库连接,这需要大量的粘性状态或一些聪明的后端路由。

这些是唯一可能的三种方法吗?如果没有,是否有计算机科学概念可以让我阅读这篇文章?有没有方法可以在不存储整个结果集的情况下逼近延续方法?从长远来看,存在复杂的事件流/时间点系统,其中“我获取第1页时的结果集”是永久可导出的。没错......?

5 个答案:

答案 0 :(得分:6)

Oracle处理得很好。只要光标处于打开状态,您就可以根据需要多次获取,结果将始终反映光标打开的时间点。它使用撤消日志中的数据虚拟回滚光标打开后提交的更改。

只要所需的回滚数据仍然可用,它就会起作用。最终日志被回收并且回滚数据不再可用,因此存在一些限制,具体取决于日志空间,系统活动等。

不幸的是(IMO),我不知道任何其他像这样工作的数据库。我使用的其他数据库使用锁来确保读取一致性,如果您希望读取一致性超过很短的持续时间,则会出现问题。

答案 1 :(得分:6)

解决方案1:“ hacky解决方案

解决方案可能包括您的客户端跟踪已经看到的内容,例如ID列表。每次需要其他页面时,都会将此ID列表添加到服务器调用的参数中。然后,您的服务器可以订购内容,删除已经看过的内容并应用偏移量以获得正确的页面。

我不推荐它,但我坚持 hacky 。我只是把它写在这里,因为它很快,可以满足一些需求。这是我能想到的坏事:

1)在客户端需要做一些工作才能做到正确(上面的句子中“已经看到”意味着什么,如果我去上一页怎么办?)

2)生成的订单并不反映您的真实订购政策。内容可以显示在第2页中,尽管该策略应该将其放在第1页上。这可能会导致用户误解。让我们以堆栈溢出为例,使用其以前的排序策略,这意味着首先是最受欢迎的答案。我们可以在第2页中提出6个upvotes的问题,而在第1页有4个upvotes的问题。当用户仍在第1页时发生2个或更多的upvotes时会发生这种情况。 - >对于用户来说可能会令人惊讶。

解决方案2 客户端解决方案”

它基本上是客户端等效的解决方案,你称之为“服务器端状态”。只有在服务器端跟踪完整订单不够方便时才有用。如果项目列表不是无限的,它就可以工作。

  • 致电您的服务器以获取完整(有限)订单列表+项目数/页
  • 将其保存在客户端
  • 直接通过内容的ID检索项目。

答案 2 :(得分:4)

我们现在使用服务器端状态方法,在第一个查询上缓存整个结果,因此我们总是返回一致的列表。只要我们的查询已经返回所有行,这将起作用;最终我们需要使用最近邻法并且不会起作用。

但我认为只要有第四种可能性,它可以很好地扩展:

  1. 您不需要保证没有重复,只有很高的可能性
  2. 只要您避免重复,您就可以在滚动期间丢失某些内容。
  3. 解决方案是“最后见过的ID”解决方案的变体:让客户端保留一个,但不是5个或10个或20个书签 - 足够少,您可以有效地存储它们。查询最终看起来像:

    SELECT * FROM posts
    WHERE id > :bookmark_1
    AND id > :bookmark_2
    ...
    ORDER BY id
    

    随着书签数量的增加,你可能会迅速减少(a)从所有n个书签开始,但是(b)看到重复的内容,因为它们都被重新排列。

    如果将来有漏洞或更好的答案,我很乐意接受这个答案。

答案 3 :(得分:1)

聚会很晚,但这是我们尝试过的东西。我们使用的是连续加载,而不是用户之间来回切换的页面。

客户端构建了它显示的所有ID的列表,因此在第一次设置之后它可能是: 4,7,19,2,1,72,3

当我们加载更多内容时,我们使用相同的排序执行相同的查询,但是将其添加到它: 在哪里不是(4,7,19,2,1,72,3)

NOT IN列表可以快速增长。对我们来说,这不是一个问题,因为我们的内部工具通常没有大量的结果。

我想补充一点想法。也许服务器端添加可以应用于此。当用户搜索时,将他们获得的所有ID添加到包含搜索链接的表中。当客户想要更多时,它只需提供搜索ID(或使用服务器端状态),查询可以加入他们的搜索数据。

答案 4 :(得分:0)

如果行包含创建时间戳记,则查询可以包含“之前”过滤器。这样可确保不包括在时间戳之后创建的任何行,因此分页是一致的(前提是这些行在恒定列上排序)。这是一个示例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)的下一页。因此,客户端保留了请求下一页所需的所有状态,并且服务器在分页方面可以保持无状态。

注意:如果用户刷新的URL不带before参数(即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参数以查看新数据。