Offset Pagination — пагинация со смещением — метод постраничной загрузки данных через HTTP API. Клиент передаёт параметры offset (смещение от начала) и limit (размер страницы), а сервер возвращает записи с позиции offset. По данным REST API Tutorial, этот подход широко применяется в RESTful-сервисах благодаря простоте реализации. Однако на больших объёмах данных offset-пагинация теряет производительность из-за полного сканирования таблицы до нужной позиции.
Главное
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 записей на страницу в зависимости от сложности данных.
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 преобразуется в SQL-запрос с конструкциями OFFSET и FETCH NEXT (или LIMIT в MySQL/SQLite). Сервер базы данных сканирует таблицу, пропускает количество строк, равное offset, и возвращает следующие limit строк. Чем больше offset, тем дольше выполняется запрос.
Проблема производительности связана с тем, что база данных не может перейти сразу к позиции offset — она должна прочитать и отбросить все предыдущие строки. При offset = 100000 и limit = 20 СУБД прочитает 100020 строк и вернёт только 20.
SQL — язык, на котором сервер выполняет offset-пагинацию. В PostgreSQL и MySQL используется LIMIT, в SQL Server и Oracle — OFFSET...FETCH. Разные СУБД оптимизируют этот запрос по-своему, но фундаментальная проблема сканирования остаётся той же.
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.
Cursor-based pagination — альтернатива Offset Pagination, использующая указатель на последнюю запись текущей страницы. Вместо числового смещения клиент передаёт идентификатор последней полученной записи, а сервер возвращает следующие N записей после неё.
Cursor-based подход решает проблему консистентности: положение курсора не меняется при вставках или удалениях, так как курсор ссылается на конкретную запись, а не на позицию. Однако он сложнее в реализации — требуется уникальное сортируемое поле (обычно ID или timestamp).
| Параметр | Offset Pagination | Cursor-based Pagination |
|---|---|---|
| Простота | Высокая — два числовых параметра | Средняя — требуется кодирование курсора |
| Консистентность | Низкая — дубликаты при вставках | Высокая — курсор не зависит от изменений |
| Производительность | Падает с ростом offset | Стабильна на любых объёмах |
| Прыжок на страницу | Да — можно перейти на любую страницу | Нет — только последовательная навигация |
| Подходит для | Таблиц <10K записей, UI с номерами страниц | Лент, бесконечного скролла, больших наборов |
Выбор между подходами зависит от требований к интерфейсу пользователя. Если нужна навигация с номерами страниц и прямым переходом — Offset Pagination проще. Для бесконечного скролла или ленты новостей предпочтительнее курсоры.
Keyset pagination — разновидность cursor-based подхода, при которой фильтрация выполняется по уникальному ключу с помощью WHERE вместо OFFSET. SQL-запрос использует условие WHERE id > lastId, что позволяет базе данных использовать индекс без сканирования отброшенных строк.
По данным PostgreSQL Wiki, keyset pagination выполняется в 100-1000 раз быстрее offset-запроса при больших смещениях, так как индексное сканирование заменяет полный обход таблицы. Недостаток — невозможность прыжка на произвольную страницу без последовательного прохода.
Offset Pagination оптимальна для небольших и средних наборов данных (до 10000 записей), где пользователю нужен интерфейс с номерами страниц. Типовые сценарии — админ-панели, списки заказов, каталоги с фильтрацией и пагинацией по страницам.
Для мобильных приложений offset-пагинация подходит при загрузке исторических данных, где вставки новых записей редки или невозможны — например, история заказов пользователя, список завершённых задач, архив транзакций. В этих сценариях проблема консистентности не возникает.
Не рекомендуется использовать Offset Pagination для лент социальных сетей, списков комментариев, чатов и других динамических наборов с частыми вставками. В этих случаях пропуски и дубликаты записей ухудшают пользовательский опыт и требуют дополнительной логики дедупликации на клиенте.
Гибридная пагинация комбинирует offset и cursor: первый запрос использует offset для показа начальной страницы, а последующие — cursor для подгрузки бесконечного скролла. Этот подход реализуется в Instagram и Twitter, где первая страница загружается через cursor, но offset используется для расчёта позиции при возврате к предыдущему просмотру.
Реализация гибридного подхода требует хранения виртуальной позиции пользователя на клиенте и согласования двух механизмов пагинации на сервере. По данным блога Instagram Engineering, их команда использует cursor-based пагинацию с дополнительным полем startCursor, которое заменяет offset для начальной загрузки.
Мобильные приложения используют Offset Pagination в паре с Retrofit/OkHttp на Android и URLSession/Combine на iOS. Типичный паттерн — загрузка следующей страницы при скролле к концу списка через RecyclerView.OnScrollListener или UICollectionView prefetching.
Реализация offset-пагинации на мобильном клиенте включает три компонента: менеджер пагинации (хранит текущий offset и hasMore), адаптер списка (отображает элементы и индикатор загрузки) и репозиторий (выполняет запросы и обрабатывает ошибки). Android Jetpack предлагает Paging 3 Library, которая поддерживает как offset, так и cursor-based пагинацию из коробки.
Paging 3 — библиотека Android Jetpack для постраничной загрузки данных. Она инкапсулирует логику пагинации, включая отслеживание offset, управление состоянием загрузки и автоматическую подгрузку при скролле. PagingSource определяет ключи для следующей и предыдущей страниц.
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 требует стабильной сортировки ORDER BY по уникальному полю. Без неё СУБД может возвращать записи в произвольном порядке, что приводит к случайным дубликатам и пропускам между страницами.
Вторая ошибка — использовать offset для расчёта номера страницы в UI. Формула page = offset / limit + 1 работает только при условии, что ни одна запись не была удалена или добавлена между загрузками. При динамических данных номер страницы становится неточным, и пользователь видит некорректную информацию.
Третья ошибка — игнорировать таймауты запросов с большим offset. При offset свыше 100000 запрос может выполняться десятки секунд, блокируя UI и потребляя ресурсы сервера. Рекомендуется устанавливать максимальное значение offset на уровне API (например, 10000) и использовать cursor-based пагинацию для больших объёмов.
Четвёртая ошибка — не добавлять total count в ответ. Без общего количества записей клиент не может отобразить число страниц и реализовать пагинацию с номерами. Однако вычисление COUNT(*) на больших таблицах тоже дорого — для наборов свыше 100000 записей используйте приблизительные оценки или ограничьте максимальное значение total.
Часто задаваемые вопросы
Offset использует числовое смещение (offset) для пропуска записей, а cursor — указатель на последнюю запись предыдущей страницы. Offset проще в реализации, но страдает от дубликатов при вставках и потери производительности на больших смещениях. Cursor стабилен при любых изменениях данных.
Offset пагинация неэффективна при offset более 10000 записей из-за полного сканирования таблицы. Также она непригодна для динамических наборов (ленты, чаты), где новые записи появляются между запросами — пользователь видит пропуски и дубликаты записей при навигации.
Оптимальный limit зависит от размера записи и скорости сети — от 10 до 50 элементов на страницу. Для списков с большими изображениями используйте limit = 10-15, для текстовых данных — 20-50. Всегда разрешайте клиенту указывать свой limit с ограничением максимума на сервере (обычно 100).
Для борьбы с дубликатами используйте дедупликацию на клиенте по уникальному ID, применяйте stable sorting по уникальному полю или переходите на cursor-based пагинацию. Android Paging 3 поддерживает key для автоматической дедупликации элементов списка.
Да, GraphQL поддерживает offset-пагинацию через аргументы offset и limit в запросе, хотя спецификация Relay рекомендует cursor-based подход. Библиотеки Apollo GraphQL и Relay предлагают встроенную поддержку offset-пагинации с автоматическим управлением состоянием страниц.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также