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 включва параметри на заявката 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 и курсор: първата заявка използва offset за показване на началната страница, а следващите — курсор за зареждане на безкраен скрол. Този подход е реализиран в Instagram и Twitter, където първата страница се зарежда чрез курсор, но 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 за изчисляване на номера на страница в интерфейса. Формулата page = offset / limit + 1 работи само при условие, че нито един запис не е бил изтрит или добавен между зарежданията. При динамични данни номерът на страницата става неточен и потребителят вижда некоректна информация.
Трета грешка — игнориране на таймаутите на заявки с голям offset. При offset над 100000, заявката може да отнеме десетки секунди, блокирайки интерфейса и изразходвайки ресурси на сървъра. Препоръчително е да се зададе максимална стойност на offset на ниво API (например 10000) и да се използва cursor-based пагинация за големи обеми.
Четвърта грешка — недобавяне на total count в отговора. Без общия брой записи, клиентът не може да покаже броя на страниците и да реализира пагинация с номера. Въпреки това, изчисляването на COUNT(*) на големи таблици също е скъпо — за набори над 100000 записа използвайте приблизителни оценки или ограничете максималната стойност на total.
Често задавани въпроси
Offset използва числово отместване (offset) за пропускане на записи, а курсорът — указател към последния запис на предишната страница. Offset е по-прост за реализация, но страда от дубликати при вмъкване и загуба на производителност при големи отмествания. Курсорът е стабилен при всякакви промени в данните.
Offset пагинацията е неефективна при offset над 10000 записа поради пълно сканиране на таблицата. Също така е неподходяща за динамични набори (потоци, чатове), където нови записи се появяват между заявките — потребителят вижда пропуски и дубликати при навигация.
Оптималният limit зависи от размера на записа и скоростта на мрежата — от 10 до 50 елемента на страница. За списъци с големи изображения използвайте limit = 10-15, за текстови данни — 20-50. Винаги позволявайте на клиента да посочи свой limit с максимално ограничение на сървъра (обикновено 100).
За борба с дубликатите използвайте дедупликация на клиента по уникален ID, приложете стабилно сортиране по уникално поле или преминете на cursor-based пагинация. Android Paging 3 поддържа ключ за автоматична дедупликация на елементи от списъка.
Да, GraphQL поддържа offset-пагинация чрез аргументите offset и limit в заявката, въпреки че спецификацията Relay препоръчва cursor-based подход. Библиотеките Apollo GraphQL и Relay предлагат вградена поддръжка за offset-пагинация с автоматично управление на състоянието на страниците.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също