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 са бројевима страница | Feed-ове, бесконачни скрол, велике скупове |
Избор између приступа зависи од захтева интерфејса корисника. Ако је потребна навигација са бројевима страница и директним преласком — Offset Pagination је једноставнији. За бесконачни скрол или feed-ове вести, курсори су пожељнији.
Keyset pagination — варијанта cursor-based приступа, при којој се филтрирање врши по јединственом кључу помоћу WHERE уместо OFFSET-а. SQL упит користи услов WHERE id > lastId, што омогућава бази података да користи индекс без скенирања одбачених редова.
Према PostgreSQL Wiki, keyset pagination се извршава 100-1000 пута брже од offset упита при великим померајима, јер индексно скенирање замењује потпуни обилазак табеле. Недостатак — немогућност скока на произвољну страницу без секвенцијалног проласка.
Offset Pagination је оптимална за мале и средње скупове података (до 10000 записа), где кориснику треба интерфејс са бројевима страница. Типични сценарији — админ панели, листе поруџбина, каталози са филтрирањем и пагинацијом по страницама.
За мобилне апликације, offset-пагинација је погодна при учитавању историјских података, где су уметања нових записа ретка или немогућа — на пример, историја поруџбина корисника, листа завршених задатака, архива трансакција. У овим сценаријима проблем конзистентности се не јавља.
Не препоручује се коришћење Offset Pagination-а за feed-ове друштвених мрежа, листе коментара, четове и друге динамичке скупове са честим уметањима. У овим случајевима, прескакања и дупликати записа погоршавају корисничко искуство и захтевају додатну логику дедупликације на клијенту.
Хибридна пагинација комбинује 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-а за израчунавање броја странице у интерфејсу. Формула page = offset / limit + 1 ради само под условом да ниједан запис није обрисан или додат између учитавања. При динамичким подацима, број странице постаје нетачан, а корисник види некоректне информације.
Трећа грешка — игнорисање тајмаута упита са великим offset-ом. При offset-у изнад 100000, упит може трајати десетинама секунди, блокирајући интерфејс и трошећи ресурсе сервера. Препоручује се постављање максималне вредности offset-а на нивоу API-ја (на пример, 10000) и коришћење cursor-based пагинације за велике обимe.
Четврта грешка — недодавање total count у одговор. Без укупног броја записа, клијент не може да прикаже број страница и имплементира пагинацију са бројевима. Међутим, израчунавање COUNT(*) на великим табелама је такође скупо — за скупове преко 100000 записа користите приближне процене или ограничите максималну вредност total.
Често постављана питања
Offset користи нумерички померај (offset) за прескакање записа, а cursor — показивач на последњи запис претходне странице. Offset је једноставнији за имплементацију, али пати од дупликата при уметањима и губитка перформанси при великим померајима. Cursor је стабилан при било каквим променама података.
Offset пагинација је неефикасна при offset-у преко 10000 записа због потпуног скенирања табеле. Такође је неприкладна за динамичке скупове (feed-ови, четови) где се нови записи појављују између захтева — корисник види прескакања и дупликате записа при навигацији.
Оптимални 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. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође