Пагинация сложна, когда рейтинг вашего контента может быстро меняться, и еще сложнее, когда эти рейтинги различаются для каждого пользователя. (Давайте рассматривать бесконечную прокрутку как тип разбиения на страницы, где ссылки невидимы.) Есть две серьезные проблемы: недавно добавленный контент вверху и контент с измененным рейтингом.
Давайте забудем о недавно добавленном контенте и согласимся с тем, что вам придется обновить страницу 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 и Каминари работают. Если я хочу получить страницу 2, я сделаю
SELECT * FROM animals
ORDER BY animals.position
OFFSET ((:page_num - 1) * :page_size)
LIMIT :page_size;
который получает строки 5-8. Слонов я никогда не увижу, а коров увижу дважды.
Идентификатор последнего увиденного
Reddit использует другой подход. Вместо того, чтобы вычислять первую строку на основе размера страницы, клиент отслеживает идентификатор последнего элемента, который вы видели, как закладку. Когда вы нажимаете «Далее», они начинают искать с этой закладки и далее:
SELECT * FROM animals
WHERE position > (
SELECT position FROM animals
WHERE id = :last_seen_id
)
ORDER BY position
LIMIT :page_size;
В некоторых случаях это работает лучше, чем страница/смещение. Но в нашем случае «Собаки», последний просмотренный пост, увеличился до № 1. Итак, клиент отправляет ?last_seen_id=4
, а моя страница 2 — это летучие мыши, альпаки, слоны и лисы. Я не пропустил ни одного животного, но дважды видел летучих мышей и альпак.
Состояние на стороне сервера
HackerNews (и наш сайт прямо сейчас) решает эту проблему с помощью продолжений на стороне сервера; они сохраняют для вас полный набор результатов (или, по крайней мере, несколько страниц заранее?), а ссылка "Дополнительно" ссылается на это продолжение. Когда я получаю страницу 2, я запрашиваю «страницу 2 моего исходного запроса». Он использует тот же расчет смещения/предела, но, поскольку он противоречит исходному запросу, меня просто не волнует, что теперь все изменилось. Я вижу слонов, лис, жирафов и лошадей. Нет дубликатов, нет пропущенных элементов.
Недостатком является то, что мы должны хранить много состояния на сервере. В HN это хранится в ОЗУ, и на самом деле эти продолжения часто истекают, прежде чем вы сможете нажать кнопку «Дополнительно», что вынуждает вас вернуться на страницу 1, чтобы найти действительную ссылку. В большинстве приложений вы можете сохранить это в memcached или даже в самой базе данных (используя свою собственную таблицу или в Oracle или PostgreSQL, используя удерживаемые курсоры). В зависимости от вашего приложения производительность может снизиться; по крайней мере, в PostgreSQL вам нужно найти способ снова установить правильное соединение с базой данных, что требует большого количества фиксированных состояний или какой-то умной внутренней маршрутизации.
Это единственные три возможных подхода? Если нет, то существуют ли концепции информатики, которые дадут мне возможность прочитать об этом в Google? Существуют ли способы аппроксимации метода продолжения без сохранения всего набора результатов? В долгосрочной перспективе существуют сложные системы потоковой передачи событий/на определенный момент времени, в которых «набор результатов на момент, когда я извлек страницу 1», можно вывести навсегда. Если не считать...?