Блог

Limit/offset vs pageNumber/pageSize

October 12, 2020

Сегодня меня осенило по части того какому же стилю пагинации лучше отдать предпочтение. Речь идет о двух вариантах:

  • limit/offset
  • pageNumber/pageSize

В первом приближении оба стиля +/- одинаковые. Один более “технический” и привязан к работе с данными в бд, а второй больше наследует логику работы пагинации с точки зрения юзера. В плане использования в коде оба подхода особо не создают проблем, ведь пересчитать с лимитов на страницы и обратно не составляет труда:

const getLimit = (pageNumber, pageSize) => pageSize;
const getOffset = (pageNumber, pageSize) => (pageNumber - 1) * pageSize;
const getPageSize = (offset, limit) => limit;
const getPageNumber = (offset, limit) => Math.floor(offset / limit) + 1;

По этой причине, всегда, когда надо было решать, какой из стилей использовать, это создавало аналитический паралич (лайт версию). Внутренне я больше предпочитаю limit/offset так как он более абстрактный, но если на каком-то проекте уже начали использовать pageNumber/pageSize, я обычно не бежал переписывать все на limit/offset, а просто продолжал везде следовать одному и тому же стилю.

Если вас интересует TL;DR версия этого поста, просто проскрольте в самый низ.

История из жизни

Как я уже говорил, обычно я не могу четко определиться какой стиль пагинации лучше, но сегодня у меня случился прямо-таки инсайт, который предоставил мне довольно весомые аргументы в пользу limit/offset (ладно, на самом деле уже прошла где-то неделя, пока я доделал пост).

А дело было вот в чем: на одном проекте, мне необходимо было вывести большой список данных, если точнее 2к+ итемов. При этом весь список весил более 2.5MB и, в силу того, что каждый итем был результатом аггрегации нескольких сущностей, то генерация такого списка при не прогретом кеше занимала более 5 секунд. С прогретым кешом чуть лучше — в раене 1с. Но в любом случае, это слишком долго даже для такого второстепенного приложения, как админка. Варианты типа “выводить часть списка” или “отказаться от дорогой агрегации на бекенде” на том этапе было затруднительно, так как для этого пришлось бы переписать немалый кусок проекта.

В общем, это все была предыстория. Из-за определенных ограничений, я решил немного оптимизировать принцип работы с апи. Идея в том, чтобы забирать весь список, но в несколько запросов. При чем, в идеале забрать первую часть списка маленьким, быстрым запросом, например элементов 50, а вторую часть списка забрать одним/несколькими запросами, но уже с большим кол-вом элементов, например 500 (или даже 1000, 2000). И вот тут появляются проблемы. Если размер первой страницы 50, то чтобы дальше получать по 500 элементов, я должен сначала запросить еще 9 страниц по 50 элементов, чтобы дальше можно было переключиться на пагинацию по 500 элементов начиная со второй страницы:

  • pageNumber=1&pageSize=50
  • pageNumber=10&pageSize=50 — это последняя порция запросов по 50 элементов, которая прибавит 50 итемов к 450 запрошенным ранее
  • pageNumber=2&pageSize=500 — переходим на пагинацию по 500 итемов, начиная с 501го итема

Ну или начать запрашивать с первой страницы. Просто первые 50 итемов, вероятно, совпадут ранее полученными и их надо будет отфильтровать/смерджить:

  • pageNumber=1&pageSize=50
  • pageNumber=1&pageSize=500 — сравниваем данные и исключаем дубли
  • pageNumber=2&pageSize=500

В случае, если бы я использовал limit/offset, то мне достаточно было сделать просто два запроса:

  • limit=50&offset=0
  • limit=500&offset=50

Вот реальный пример ситуации, когда limit/offset выигрывает у pageNumber/pageSize.

Другие примеры использования limit/offset

Описанный выше случай, не единственный случай, когда limit/offset упрощают жизнь. Есть более простой случай с пагинатором, который предоставляет динамически менять размер страницы:

Пагинатор с выбором размера страницы

Если попытаться заимплементить такой пагинатор используя API с pageNumber/pageSize, у вас будут проблемы с пересчетом правильной страницы при смене ее pageSize. Скорее всего вам придется показывать часть предыдущей страницы, которую пользователь уже видел.

В случае использования limit/offset вам достаточно просто поменять значение limit и продолжить с того же offset.

TL;DR

Использование limit/offset дает возможность динамически менять размер страницы находясь где-то посредине списка, в то время как с pageNumber/pageSize довольно сложно поменять размер страницы, находясь не на первой странице. Придется либо использовать какие-то кратные размеры страниц, либо смириться с тем. что после изменения размера страницы, список элементов может слегка сместиться.