Какие схемы разбиения на страницы могут обрабатывать быстро меняющиеся списки контента?

Pagination сложна, когда ваши рейтинги контента могут быстро меняться и даже сложнее, если эти рейтинги отличаются для каждого пользователя. (Позвольте рассматривать бесконечную прокрутку как тип разбивки на страницы, где ссылки невидимы.) Есть две трудные проблемы: недавно добавленный контент вверху и переименованный контент.

Забудьте о недавно добавленном контенте и примите, что вам нужно будет обновить страницу 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 использует другой подход. Вместо вычисления первой строки на основе размера страницы клиент отслеживает идентификатор последнего элемента, который вы видели, как закладка. Когда вы нажмете "next", они начнут искать из этой закладки:

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", навсегда выводится. Коротко это...?

Ответ 1

Теперь мы будем использовать подход к состоянию на стороне сервера, кэшируем весь результат по первому запросу, чтобы мы всегда возвращали согласованный список. Это будет работать, пока наш запрос уже возвращает все строки; в конечном итоге нам нужно будет использовать подход ближайшего соседа, и это не сработает.

Но я думаю, что есть четвертая возможность, которая очень хорошо масштабируется, если:

  • Вам не нужна гарантия отсутствия дубликатов, только высокая вероятность
  • У вас все в порядке с отсутствием содержимого во время свитков, если вы избегаете дубликатов.

Решение представляет собой вариант решения "последний увиденный идентификатор": не держите клиента не одну, а 5 или 10 или 20 закладок - достаточно немногих, чтобы вы могли эффективно их хранить. Запрос заканчивается следующим образом:

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

По мере того, как количество закладок растет, шансы быстро уменьшаются, что вы (а) начинаете в какой-то момент после всех n закладок, но (б) все равно просматриваете дублирующийся контент, потому что все они перерегистрированы.

Если в будущем есть дыры или лучшие ответы, я с радостью не соглашусь с этим ответом.

Ответ 2

Решение 1: "хакерское решение"

Решение может состоять в том, что ваш клиент отслеживает уже увиденный контент, например список идентификаторов. Каждый раз, когда вам нужна другая страница, вы добавляете этот идентификационный список к параметрам вашего вызова сервера. Затем ваш сервер может заказать контент, удалить уже увиденное содержимое и применить смещение, чтобы получить нужную страницу.

Я бы не рекомендовал его, и я настаиваю на hacky. Я просто записываю это здесь, потому что это быстро и может соответствовать некоторым потребностям. вот плохие вещи, о которых я могу думать:

1) Требуется некоторая работа с клиентской стороной, чтобы понять это (что "уже видно" означает в моем предложении выше, что, если я перейду на предыдущую страницу?)

2) Полученный заказ не отражает вашу истинную политику заказа. Содержимое может отображаться на странице 2, хотя политика должна была помещаться на страницу 1. Это может привести к недоразумению пользователя. Возьмем пример со своей прежней политикой упорядочения, а это означает, что сначала будут рассмотрены самые приоритетные ответы. У нас может возникнуть вопрос с 6 upvotes на странице 2, в то время как вопрос с 4 upvotes будет на стр. 1. Это происходит, когда 2 или более upvotes произошли, когда пользователь все еще находился на странице 1. → может быть неожиданным для пользователя.

Решение 2: " клиентское решение"

В основном это эквивалентное решение на стороне клиента, которое вы называете "серверным состоянием". Тогда он полезен только в том случае, если отслеживать полный порядок на стороне сервера недостаточно. Он работает, если список элементов не бесконечен.

  • Позвоните своему серверу, чтобы получить полный (конечный) список заказов + количество элементов/страницы
  • Сохранить на клиентской стороне.
  • Получить элементы непосредственно через идентификаторы вашего содержимого.

Ответ 3

Oracle отлично справляется с этим. Пока курсор открыт, вы можете извлекать столько раз, сколько необходимо, и ваши результаты всегда будут отражать момент времени, в который был открыт курсор. Он использует данные из журналов отмены для практически отмененных изменений, которые были зафиксированы после открытия курсора.

Он будет работать до тех пор, пока все необходимые данные отката будут доступны. В конечном итоге журналы возвращаются в исходное состояние, и данные отката больше недоступны, поэтому в зависимости от пространства журнала, активности системы и т.д. Существует некоторый предел.

К сожалению (IMO), я не знаю ни одной другой БД, которая работает так. Другие базы данных, с которыми я работал, используют блокировки для обеспечения согласованности чтения, что является проблематичным, если вы хотите, чтобы согласованность чтения была более чем на короткое время.

Ответ 4

Очень поздно вечеринке, но здесь мы кое-что экспериментировали. Мы используем непрерывную загрузку, а не страницы, которые пользователь будет перемещать между ними.

Клиент строит список всех идентификаторов, которые он отобразил, поэтому после первого набора он может быть: 4,7,19,2,1,72,3

Когда мы загружаем больше контента, мы делаем тот же запрос с тем же типом, но добавляем к нему: WHERE id NOT IN (4,7,19,2,1,72,3)

Список NOT IN может расти довольно быстро. Для нас это не проблема, поскольку наш внутренний инструмент обычно не имеет тонны результатов.

Я хочу добавить еще одну идею. Возможно, к этому может быть добавлено дополнение на стороне сервера. Когда пользователь выполняет поиск, добавьте все идентификаторы, которые они получили в таблицу со ссылкой на их поиск. Когда клиент хочет больше, он должен только предоставить идентификатор поиска (или использовать состояние на стороне сервера), и запрос может присоединиться к их поисковым данным.