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