当您的内容排名可以快速变化时,分页很难,当每个用户的排名不同时,分页就更难了。(让我们将无限滚动视为一种链接不可见的分页。)有两个难题:顶部的新添加内容和重新排序的内容。
让我们忘记新添加的内容,并接受您必须刷新第 1 页才能看到它。让我们也假装我们在做 pure 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 页时的结果集”是永远可推导出的。还不够……?