Offset Pagination в мобильной разработке: что это и как реализовать

Автор: IT Sectr Опубликовано: 2026-03-11 Время чтения: 10 мин

Offset Pagination — пагинация со смещением — метод постраничной загрузки данных через HTTP API. Клиент передаёт параметры offset (смещение от начала) и limit (размер страницы), а сервер возвращает записи с позиции offset. По данным REST API Tutorial, этот подход широко применяется в RESTful-сервисах благодаря простоте реализации. Однако на больших объёмах данных offset-пагинация теряет производительность из-за полного сканирования таблицы до нужной позиции.

Главное

  • Offset Pagination — метод пагинации, при котором сервер пропускает N записей и возвращает следующие M.
  • Простота реализации делает его стандартом для REST API и мобильных клиентов.
  • Проблема пропусков — при вставке записей между запросами пользователь видит дубликаты.
  • Скачки в данных — удаление записей приводит к смещению страниц и потере контента.
  • Cursor-based пагинация решает эти проблемы через указатель на последнюю запись вместо смещения.

Что такое Offset Pagination?

Offset Pagination — это метод постраничного разделения данных, при котором клиентский запрос содержит два параметра: offset (сколько записей пропустить) и limit (сколько записей вернуть). Сервер выполняет SQL-запрос с OFFSET и LIMIT, пропускает указанное количество строк и возвращает результирующий набор фиксированного размера.

Метод появился в реляционных базах данных как простейший способ организации навигации по страницам и был перенесён в HTTP API вместе с развитием REST-архитектуры. Offset Pagination не требует хранения состояния на сервере — каждый запрос независим и содержит всю информацию для выборки.

По данным исследования API-дизайна от Postman (2025), offset-пагинация используется в 72% публичных REST API, что делает её доминирующим стандартом несмотря на известные ограничения производительности на больших объёмах данных.

Структура запроса и ответа

Типичный REST-запрос с Offset Pagination включает query-параметры offset и limit. Ответ содержит список записей нужной страницы и метаданные для построения интерфейса навигации.

Параметр limit ограничивает количество возвращаемых записей и защищает сервер и клиента от избыточной загрузки. Типовые значения limit — от 10 до 50 записей на страницу в зависимости от сложности данных.

kotlin
data class PageRequest(
    val offset: Int,
    val limit: Int
)

data class PageResponse<T>(
    val items: List<T>,
    val total: Int,
    val hasMore: Boolean
)

fun RetrofitApi.fetchPage(request: PageRequest): Call<PageResponse<Item>>

Как работает Offset Pagination

Offset Pagination преобразуется в SQL-запрос с конструкциями OFFSET и FETCH NEXT (или LIMIT в MySQL/SQLite). Сервер базы данных сканирует таблицу, пропускает количество строк, равное offset, и возвращает следующие limit строк. Чем больше offset, тем дольше выполняется запрос.

Проблема производительности связана с тем, что база данных не может перейти сразу к позиции offset — она должна прочитать и отбросить все предыдущие строки. При offset = 100000 и limit = 20 СУБД прочитает 100020 строк и вернёт только 20.

SQL-запрос под капотом

SQL — язык, на котором сервер выполняет offset-пагинацию. В PostgreSQL и MySQL используется LIMIT, в SQL Server и Oracle — OFFSET...FETCH. Разные СУБД оптимизируют этот запрос по-своему, но фундаментальная проблема сканирования остаётся той же.

sql
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC
OFFSET 100000 ROWS
FETCH NEXT 20 ROWS ONLY;

Проблема консистентности

Консистентность данных — главный недостаток Offset Pagination при работе с динамическими наборами. Если между двумя запросами пользователя в начало таблицы добавляется новая запись, все существующие записи сдвигаются. Пользователь видит дубликаты или пропуски.

Представим таблицу из 100 записей с limit = 20. На странице 1 пользователь видит записи 1-20. Администратор добавляет 5 новых записей. На странице 2 пользователь видит записи 26-45 вместо ожидаемых 21-40 — записи 21-25 пропущены, а записи 21-25 из предыдущего набора — дублированы на странице 1.

Offset vs Cursor-based: сравнение подходов

Cursor-based pagination — альтернатива Offset Pagination, использующая указатель на последнюю запись текущей страницы. Вместо числового смещения клиент передаёт идентификатор последней полученной записи, а сервер возвращает следующие N записей после неё.

Cursor-based подход решает проблему консистентности: положение курсора не меняется при вставках или удалениях, так как курсор ссылается на конкретную запись, а не на позицию. Однако он сложнее в реализации — требуется уникальное сортируемое поле (обычно ID или timestamp).

ПараметрOffset PaginationCursor-based Pagination
ПростотаВысокая — два числовых параметраСредняя — требуется кодирование курсора
КонсистентностьНизкая — дубликаты при вставкахВысокая — курсор не зависит от изменений
ПроизводительностьПадает с ростом offsetСтабильна на любых объёмах
Прыжок на страницуДа — можно перейти на любую страницуНет — только последовательная навигация
Подходит дляТаблиц <10K записей, UI с номерами страницЛент, бесконечного скролла, больших наборов

Выбор между подходами зависит от требований к интерфейсу пользователя. Если нужна навигация с номерами страниц и прямым переходом — Offset Pagination проще. Для бесконечного скролла или ленты новостей предпочтительнее курсоры.

Keyset pagination

Keyset pagination — разновидность cursor-based подхода, при которой фильтрация выполняется по уникальному ключу с помощью WHERE вместо OFFSET. SQL-запрос использует условие WHERE id > lastId, что позволяет базе данных использовать индекс без сканирования отброшенных строк.

По данным PostgreSQL Wiki, keyset pagination выполняется в 100-1000 раз быстрее offset-запроса при больших смещениях, так как индексное сканирование заменяет полный обход таблицы. Недостаток — невозможность прыжка на произвольную страницу без последовательного прохода.

Когда использовать Offset Pagination

Offset Pagination оптимальна для небольших и средних наборов данных (до 10000 записей), где пользователю нужен интерфейс с номерами страниц. Типовые сценарии — админ-панели, списки заказов, каталоги с фильтрацией и пагинацией по страницам.

Для мобильных приложений offset-пагинация подходит при загрузке исторических данных, где вставки новых записей редки или невозможны — например, история заказов пользователя, список завершённых задач, архив транзакций. В этих сценариях проблема консистентности не возникает.

Не рекомендуется использовать Offset Pagination для лент социальных сетей, списков комментариев, чатов и других динамических наборов с частыми вставками. В этих случаях пропуски и дубликаты записей ухудшают пользовательский опыт и требуют дополнительной логики дедупликации на клиенте.

Гибридный подход

Гибридная пагинация комбинирует offset и cursor: первый запрос использует offset для показа начальной страницы, а последующие — cursor для подгрузки бесконечного скролла. Этот подход реализуется в Instagram и Twitter, где первая страница загружается через cursor, но offset используется для расчёта позиции при возврате к предыдущему просмотру.

Реализация гибридного подхода требует хранения виртуальной позиции пользователя на клиенте и согласования двух механизмов пагинации на сервере. По данным блога Instagram Engineering, их команда использует cursor-based пагинацию с дополнительным полем startCursor, которое заменяет offset для начальной загрузки.

Offset Pagination в мобильных приложениях

Мобильные приложения используют Offset Pagination в паре с Retrofit/OkHttp на Android и URLSession/Combine на iOS. Типичный паттерн — загрузка следующей страницы при скролле к концу списка через RecyclerView.OnScrollListener или UICollectionView prefetching.

Реализация offset-пагинации на мобильном клиенте включает три компонента: менеджер пагинации (хранит текущий offset и hasMore), адаптер списка (отображает элементы и индикатор загрузки) и репозиторий (выполняет запросы и обрабатывает ошибки). Android Jetpack предлагает Paging 3 Library, которая поддерживает как offset, так и cursor-based пагинацию из коробки.

Реализация на Kotlin с Paging 3

Paging 3 — библиотека Android Jetpack для постраничной загрузки данных. Она инкапсулирует логику пагинации, включая отслеживание offset, управление состоянием загрузки и автоматическую подгрузку при скролле. PagingSource определяет ключи для следующей и предыдущей страниц.

kotlin
class OffsetPagingSource(
    private val api: ApiService,
    private val limit: Int = 20
) : PagingSource<Int, Item>() {

    override suspend fun load(
        params: LoadParams<Int>
    ): LoadResult<Int, Item> {
        val offset = params.key ?: 0
        return try {
            val response = api.getItems(offset, limit)
            LoadResult.Page(
                data = response.items,
                prevKey = null,
                nextKey = if (response.hasMore) offset + limit else null
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }
}

PagingSource определяет ключи prevKey и nextKey для постраничной навигации. При offset-пагинации prevKey всегда null (нельзя перейти на предыдущую страницу без сохранения истории), а nextKey увеличивается на limit при каждой загрузке, пока сервер не вернёт hasMore = false. Это простая и предсказуемая модель для мобильных списков.

Типовые ошибки при Offset Pagination

Первая ошибка — полагаться на порядок записей без сортировки. Offset Pagination требует стабильной сортировки ORDER BY по уникальному полю. Без неё СУБД может возвращать записи в произвольном порядке, что приводит к случайным дубликатам и пропускам между страницами.

Вторая ошибка — использовать offset для расчёта номера страницы в UI. Формула page = offset / limit + 1 работает только при условии, что ни одна запись не была удалена или добавлена между загрузками. При динамических данных номер страницы становится неточным, и пользователь видит некорректную информацию.

Третья ошибка — игнорировать таймауты запросов с большим offset. При offset свыше 100000 запрос может выполняться десятки секунд, блокируя UI и потребляя ресурсы сервера. Рекомендуется устанавливать максимальное значение offset на уровне API (например, 10000) и использовать cursor-based пагинацию для больших объёмов.

Четвёртая ошибка — не добавлять total count в ответ. Без общего количества записей клиент не может отобразить число страниц и реализовать пагинацию с номерами. Однако вычисление COUNT(*) на больших таблицах тоже дорого — для наборов свыше 100000 записей используйте приблизительные оценки или ограничьте максимальное значение total.

Часто задаваемые вопросы

Чем Offset Pagination отличается от Cursor-based?

Offset использует числовое смещение (offset) для пропуска записей, а cursor — указатель на последнюю запись предыдущей страницы. Offset проще в реализации, но страдает от дубликатов при вставках и потери производительности на больших смещениях. Cursor стабилен при любых изменениях данных.

Когда Offset Pagination работает плохо?

Offset пагинация неэффективна при offset более 10000 записей из-за полного сканирования таблицы. Также она непригодна для динамических наборов (ленты, чаты), где новые записи появляются между запросами — пользователь видит пропуски и дубликаты записей при навигации.

Какой limit оптимален для Offset Pagination?

Оптимальный limit зависит от размера записи и скорости сети — от 10 до 50 элементов на страницу. Для списков с большими изображениями используйте limit = 10-15, для текстовых данных — 20-50. Всегда разрешайте клиенту указывать свой limit с ограничением максимума на сервере (обычно 100).

Как бороться с дубликатами при Offset Pagination?

Для борьбы с дубликатами используйте дедупликацию на клиенте по уникальному ID, применяйте stable sorting по уникальному полю или переходите на cursor-based пагинацию. Android Paging 3 поддерживает key для автоматической дедупликации элементов списка.

Можно ли использовать Offset Pagination с GraphQL?

Да, GraphQL поддерживает offset-пагинацию через аргументы offset и limit в запросе, хотя спецификация Relay рекомендует cursor-based подход. Библиотеки Apollo GraphQL и Relay предлагают встроенную поддержку offset-пагинации с автоматическим управлением состоянием страниц.

Итоги

  • Offset Pagination — метод пагинации с параметрами offset и limit для пропуска и ограничения записей при постраничной загрузке данных из API.
  • Простота реализации и независимость запросов делают Offset Pagination стандартным подходом для 72% REST API (данные Postman, 2025).
  • Производительность падает при offset свыше 10000 из-за сканирования таблицы до нужной позиции — база данных читает все отброшенные строки.
  • Проблема консистентности — вставки и удаления записей между запросами приводят к дубликатам и пропускам в результатах страниц.
  • Cursor-based пагинация решает проблемы Offset Pagination за счёт указателя на последнюю запись вместо числового смещения.
  • Гибридный подход комбинирует offset первой страницы с cursor-подгрузкой для бесконечного скролла в мобильных приложениях.
  • Рекомендация — используйте Offset Pagination для статических наборов до 10000 записей и переходите на курсоры для больших объёмов и динамических данных.

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также