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 використовує числове зміщення для пропуску записів, а 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

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