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

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

Давайте забудем о недавно добавленном контенте и согласимся с тем, что вам придется обновить страницу 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», можно вывести навсегда. Если не считать...?


person Jay Levitt    schedule 07.03.2012    source источник
comment
Я бы предложил посмотреть на это под другим углом. Возможно, можно вообще избежать нумерации страниц — просто используйте бесконечную прокрутку + несколько расширенных сценариев, которые обновляют список без перезагрузки страницы и отображают соответствующие символы ↑/↓ для удобства пользователя. Однако это зависит от вашего варианта использования. Upd: FWIW, вот связанный вопрос из UX StackExchange.   -  person Anton Strogonoff    schedule 16.03.2012
comment
Да, это не работает для нашего варианта использования... вещи постоянно пересматриваются, и вы бы не хотели, чтобы дисплей постоянно обновлялся. Отличная идея, однако.   -  person Jay Levitt    schedule 23.03.2012
comment
Вы можете хранить состояние на клиенте и отправлять все идентификаторы просмотренных записей.   -  person Gill Bates    schedule 03.12.2013
comment
Спасибо за ответ на ваш вопрос.   -  person FooBar    schedule 27.11.2014


Ответы (5)


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

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

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

person Todd Gibson    schedule 16.03.2012
comment
Оказывается, PostgreSQL также имеет удерживаемые курсоры. В Oracle вы можете нажать на этот курсор из другого соединения, подчиненного устройства и т. д.? Удерживаемые курсоры PostgreSQL основаны на диске (так что вы не жрете ОЗУ), и они также работают с журналом транзакций, но они доступны только в том же соединении, поэтому вам нужно либо обеспечить привязку, либо выполнить внутреннюю маршрутизацию. . - person Jay Levitt; 02.04.2012

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

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

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

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

2) Полученный заказ не отражает вашу истинную политику заказа. Контент мог отображаться на странице 2, хотя политика должна была поместить его на страницу 1. Это могло привести к неправильному пониманию пользователем. Давайте возьмем пример переполнения стека с его прежней политикой упорядочения, что означает сначала ответы, получившие наибольшее количество голосов. У нас может быть вопрос с 6 голосами "за" на странице 2, в то время как вопрос с 4 голосами "за" будет на странице 1. Это происходит, когда 2 или более голосов "за" возникают, когда пользователь все еще находится на странице 1. --> может быть неожиданным для пользователя. .

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

По сути, это решение на стороне клиента, эквивалентное тому, которое вы называете «состоянием на стороне сервера». Это полезно только в том случае, если отслеживать полный порядок на стороне сервера недостаточно удобно. Это работает, если список элементов не бесконечен.

  • Позвоните на свой сервер, чтобы получить полный (конечный) список заказов + количество товаров на странице.
  • Сохраните его на стороне клиента
  • Извлекайте элементы напрямую через идентификаторы вашего контента.
person Aurelien Porte    schedule 29.05.2014

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

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

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

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

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

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

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

person Jay Levitt    schedule 01.04.2012
comment
что вы делаете здесь, если ваш идентификатор является UUID - person PirateApp; 03.10.2019

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

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

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

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

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

person Sean256    schedule 11.04.2015

Если строки содержат отметку времени создания, запрос может включать фильтр «до». Это гарантирует, что любые строки, созданные после метки времени, не будут включены, и, следовательно, нумерация страниц будет последовательной (при условии, что строки отсортированы по постоянным столбцам). Вот пример 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), он увидит новые данные. Если пользователь обновит URL-адрес с параметром before (то есть http://some.server.com/animals?before=2020-04-08T10:40:34.833Z&page_num=2&page_size=20), он увидит те же данные. Пользователь всегда может изменить или удалить параметр before, чтобы увидеть новые данные.

person Nathan    schedule 08.04.2020