[SQL, API] Дизайн пагинации страниц в API (перевод)

Автор Сообщение
news_bot ®

Стаж: 6 лет 7 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
19-Янв-2021 03:33

Для 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», то придётся использовать старый метод. Но в других случаях метод по курсору может значительно повысить производительность, особенно на очень больших таблицах с очень глубокой пагинацией.
===========
Источник:
habr.com
===========

===========
Автор оригинала: Alexander Solovyov
===========
Похожие новости: Теги для поиска: #_sql, #_api, #_paginatsija_stranits (пагинация страниц), #_api, #_sql, #_api
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 28-Сен 11:12
Часовой пояс: UTC + 5