Как стать автором
Обновить

Дизайн пагинации страниц в API

Время на прочтение3 мин
Количество просмотров19K
Автор оригинала: Alexander Solovyov
Для API может быть сложно вернуть все результаты запроса, особенно если их тысячи. Это создаёт нагрузку на сервер, на клиент, на сеть и часто является ненужным. Поэтому и придумали пагинацию.

Обычный способ разбиения на страницы — это смещение или номер страницы. Вы делаете такой запрос:

GET /api/products?page=10
{"items": [...100 products]}

а дальше такой:

GET /api/products?page=11
{"items": [...another 100 products]}

В случае простого смещения получается ?offset=1000 и ?offset=1100 — те же яйца, только в профиль. Здесь мы либо переходим прямо к SQL-запросу типа OFFSET 1000 LIMIT 100, либо умножаем на размер страницы (значение LIMIT). В любом случае, это неоптимальное решение, поскольку каждая база данных должна пропустить эту 1000 строк. А чтобы их пропустить, нужно их идентифицировать. Неважно, это PostgreSQL, ElasticSearch или MongoDB, она должна их упорядочить, пересчитать и выбросить.

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

Что же тогда делать? Мы могли бы посмотреть, как устроены базы данных! У них есть понятие курсора — это указатель на строку. Таким образом, вы можете сказать базе данных: «Верни мне 100 строк после этой». И такой запрос гораздо удобнее для базы данных, так как высока вероятность, что вы идентифицируете строку по полю с индексом. И не нужно извлекать и пропускать эти строки, вы пройдёте прямо мимо них.

Пример:

GET /api/products
{"items": [...100 products],
 "cursor": "qWe"}

API возвращает (непрозрачную) строку, которую затем можно использовать для получения следующей страницы:

GET /api/products?cursor=qWe
{"items": [...100 products],
 "cursor": "qWr"}

С точки зрения реализации есть много вариантов. Как правило, у вас имеются некоторые критерии запроса, например, идентификатор товара (product id). В этом случае вы его кодируете с помощью некоторого обратимого алгоритма (скажем, хэш-идентификаторов). И при получении запроса с курсором вы декодируете его и генерируете запрос типа WHERE id > :cursor LIMIT 100.

Небольшое сравнение производительности. Вот результат смещения:

=# explain analyze select id from product offset 10000 limit 100;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------
Limit (cost=1114.26..1125.40 rows=100 width=4) (actual time=39.431..39.561 rows=100 loops=1)
-> Seq Scan on product (cost=0.00..1274406.22 rows=11437243 width=4) (actual time=0.015..39.123 rows=10100 loops=1)
Planning Time: 0.117 ms
Execution Time: 39.589 ms


А вот результат операции where:

=# explain analyze select id from product where id > 10000 limit 100;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------
Limit (cost=0.00..11.40 rows=100 width=4) (actual time=0.016..0.067 rows=100 loops=1)
-> Seq Scan on product (cost=0.00..1302999.32 rows=11429082 width=4) (actual time=0.015..0.052 rows=100 loops=1)
Filter: (id > 10000)
Planning Time: 0.164 ms
Execution Time: 0.094 ms


Разница в несколько порядков! Конечно, фактические цифры зависят от размера таблицы, от фильтров и реализации хранилища. Вот отличная статья с более подробной технической информацией, см. слайд 42 со сравнением производительности.


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

Минус в том, что с помощью stateless API невозможно создать ссылку «Предыдущая страница». В случае пагинации у пользователя невозможно обойти эту проблему. Так что если важно иметь кнопки предудущей/следующей страницы и «Перейти непосредственно на страницу 10», то придётся использовать старый метод. Но в других случаях метод по курсору может значительно повысить производительность, особенно на очень больших таблицах с очень глубокой пагинацией.
Теги:
Хабы:
Всего голосов 10: ↑7 и ↓3+4
Комментарии19

Публикации