Offset Pagination — lapozás eltolással — adatok oldalankénti betöltésének módszere HTTP API-n keresztül. A kliens átadja az offset (eltolás a kezdettől) és limit (oldalméret) paramétereket, a szerver pedig az offset pozíciótól kezdve adja vissza a rekordokat. A REST API Tutorial szerint ez a megközelítés széles körben használatos a RESTful szolgáltatásokban az implementáció egyszerűsége miatt. Nagy adatmennyiségeknél azonban az offset-lapozás veszít a teljesítményből a tábla teljes beolvasása miatt a kívánt pozícióig.
Főbb pontok
Offset Pagination — az adatok oldalakra osztásának módszere, ahol a kliens kérése két paramétert tartalmaz: offset (hány rekordot kihagyni) és limit (hány rekordot visszaadni). A szerver végrehajt egy SQL lekérdezést OFFSET és LIMIT utasításokkal, kihagyja a megadott számú sort, és visszaadja a rögzített méretű eredményhalmazt.
A módszer a relációs adatbázisokban jelent meg, mint a legegyszerűbb módja az oldalak közötti navigáció megszervezésének, és a REST architektúra fejlődésével került át a HTTP API-ba. Az Offset Pagination nem igényel állapottárolást a szerveren — minden kérés független és tartalmazza a lekéréshez szükséges összes információt.
A Postman (2025) API-tervezési kutatása szerint az offset-lapozást a nyilvános REST API-k 72%-a használja, ami az ismert teljesítménykorlátozások ellenére domináns szabvánnyá teszi nagy adatmennyiségek esetén.
Egy tipikus REST kérés Offset Pagination-nel tartalmazza a offset és limit lekérdezési paramétereket. A válasz tartalmazza a kért oldal rekordjainak listáját és metaadatokat a navigációs felület felépítéséhez.
A limit paraméter korlátozza a visszaadott rekordok számát és védi a szervert és a klienst a túlzott terheléstől. Tipikus limit értékek — 10-től 50 rekordig oldalanként, az adatok összetettségétől függően.
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 egy SQL lekérdezéssé alakul át OFFSET és FETCH NEXT (vagy LIMIT MySQL/SQLite esetén) konstrukciókkal. Az adatbázis-szerver beolvassa a táblát, kihagyja az offset-nek megfelelő számú sort, és visszaadja a következő limit sort. Minél nagyobb az offset, annál tovább tart a lekérdezés.
A teljesítményprobléma azzal függ össze, hogy az adatbázis nem tud közvetlenül az offset pozícióra ugrani — el kell olvasnia és el kell dobnia az összes előző sort. Offset = 100000 és limit = 20 esetén a DBMS 100020 sort olvas be, és csak 20-at ad vissza.
SQL — a nyelv, amelyen a szerver az offset-lapozást végrehajtja. PostgreSQL-ben és MySQL-ben a LIMIT, SQL Server-ben és Oracle-ben az OFFSET...FETCH használatos. A különböző DBMS-ek a maguk módján optimalizálják ezt a lekérdezést, de az alapvető beolvasási probléma ugyanaz marad.
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC
OFFSET 100000 ROWS
FETCH NEXT 20 ROWS ONLY;
Adatok konzisztenciája — az Offset Pagination fő hátránya dinamikus halmazokkal való munka során. Ha a felhasználó két kérése között egy új rekord kerül a tábla elejére, az összes létező rekord eltolódik. A felhasználó duplikátumokat vagy kihagyásokat lát.
Képzeljünk el egy 100 rekordból álló táblát limit = 20 értékkel. Az 1. oldalon a felhasználó az 1-20 rekordokat látja. Az adminisztrátor hozzáad 5 új rekordot. A 2. oldalon a felhasználó a várt 21-40 helyett a 26-45 rekordokat látja — a 21-25 rekordok kimaradnak, és az előző halmaz 21-25 rekordjai duplikálódnak az 1. oldalon.
Cursor-based pagination — alternatíva az Offset Pagination-re, amely az aktuális oldal utolsó rekordjára mutató mutatót használ. Numerikus eltolás helyett a kliens átadja az utolsó kapott rekord azonosítóját, a szerver pedig visszaadja a következő N rekordot utána.
A cursor-based megközelítés megoldja a konzisztencia problémát: a kurzor pozíciója nem változik beszúrások vagy törlések esetén, mivel a kurzor egy konkrét rekordra hivatkozik, nem pedig egy pozícióra. Azonban bonyolultabb megvalósítani — egyedi rendezhető mezőt igényel (általában ID vagy timestamp).
| Paraméter | Offset Pagination | Cursor-based Pagination |
|---|---|---|
| Egyszerűség | Magas — két numerikus paraméter | Közepes — kurzor kódolása szükséges |
| Konzisztencia | Alacsony — duplikátumok beszúráskor | Magas — kurzor nem függ a változásoktól |
| Teljesítmény | Csökken az offset növekedésével | Stabil bármilyen mennyiségnél |
| Ugrás oldalra | Igen — bármely oldalra ugorhatunk | Nem — csak szekvenciális navigáció |
| Alkalmas | <10K rekord táblák, UI oldalszámokkal | Feed-ek, végtelen görgetés, nagy halmazok |
A megközelítések közötti választás a felhasználó felületi követelményeitől függ. Ha oldalszámokkal való navigációra és közvetlen ugrásra van szükség — az Offset Pagination egyszerűbb. Végtelen görgetéshez vagy hírfolyamokhoz a kurzorok az előnyösebbek.
Keyset pagination — a cursor-based megközelítés egy változata, ahol a szűrés egyedi kulcs alapján történik WHERE segítségével az OFFSET helyett. Az SQL lekérdezés a WHERE id > lastId feltételt használja, ami lehetővé teszi az adatbázis számára, hogy indexet használjon az eldobott sorok beolvasása nélkül.
A PostgreSQL Wiki szerint a keyset pagination 100-1000-szer gyorsabban hajtódik végre, mint az offset lekérdezés nagy eltolásoknál, mivel az indexbeolvasás helyettesíti a tábla teljes bejárását. Hátrány — nem lehet tetszőleges oldalra ugrani szekvenciális bejárás nélkül.
Offset Pagination optimális kis és közepes adathalmazokhoz (10000 rekordig), ahol a felhasználónak oldalszámokkal ellátott felületre van szüksége. Tipikus forgatókönyvek — admin panelek, rendelési listák, szűréssel és oldalankénti lapozással ellátott katalógusok.
Mobil alkalmazások esetén az offset-lapozás akkor megfelelő, amikor történeti adatokat töltünk be, ahol új rekordok beszúrása ritka vagy lehetetlen — például a felhasználó rendelési előzményei, befejezett feladatok listája, tranzakciós archívum. Ezekben a forgatókönyvekben a konzisztencia probléma nem merül fel.
Nem ajánlott az Offset Pagination használata közösségi média feed-ek, kommentlisták, chat-ek és más gyakori beszúrásokkal rendelkező dinamikus halmazok esetén. Ezekben az esetekben a rekordok kihagyása és duplikálódása rontja a felhasználói élményt, és további deduplikációs logikát igényel a kliens oldalon.
Hibrid lapozás kombinálja az offset-et és a kurzort: az első kérés offset-et használ a kezdő oldal megjelenítésére, a következők pedig kurzort a végtelen görgetés betöltéséhez. Ezt a megközelítést az Instagram és a Twitter alkalmazza, ahol az első oldal kurzoron keresztül töltődik be, de az offset-et a pozíció kiszámításához használják az előző nézetbe való visszatéréskor.
A hibrid megközelítés megvalósítása megköveteli a felhasználó virtuális pozíciójának tárolását a kliens oldalon és két lapozási mechanizmus összehangolását a szerveren. Az Instagram Engineering blogja szerint csapatuk cursor-based lapozást használ egy további startCursor mezővel, amely az offset-et helyettesíti a kezdeti betöltéshez.
Mobil alkalmazások az Offset Pagination-t a Retrofit/OkHttp-val Androidon és az URLSession/Combine-nal iOS-en használják. A tipikus minta — a következő oldal betöltése a lista végére görgetéskor RecyclerView.OnScrollListener vagy UICollectionView prefetching segítségével.
Az offset-lapozás megvalósítása mobil kliensen három komponenst foglal magában: lapozáskezelő (tárolja az aktuális offset-et és a hasMore-t), lista adapter (megjeleníti az elemeket és a betöltés jelzőjét) és repository (végrehajtja a kéréseket és kezeli a hibákat). Az Android Jetpack kínálja a Paging 3 Library-t, amely mind az offset, mind a cursor-based lapozást támogatja a dobozból kivéve.
Paging 3 — Android Jetpack könyvtár adatok oldalankénti betöltéséhez. Beágyazza a lapozás logikáját, beleértve az offset követését, a betöltési állapot kezelését és az automatikus betöltést görgetéskor. A PagingSource határozza meg a következő és előző oldal kulcsait.
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 határozza meg a prevKey és nextKey kulcsokat az oldalnavigációhoz. Offset-lapozásnál a prevKey mindig null (nem lehet az előző oldalra lépni előzmények mentése nélkül), a nextKey pedig minden betöltéskor limit-tel nő, amíg a szerver hasMore = false értéket nem ad vissza. Ez egy egyszerű és kiszámítható modell mobil listákhoz.
Első hiba — a rekordok sorrendjére hagyatkozni rendezés nélkül. Az Offset Pagination megköveteli a stabil ORDER BY rendezést egy egyedi mezőn. Enélkül a DBMS tetszőleges sorrendben adhatja vissza a rekordokat, ami véletlenszerű duplikátumokhoz és kihagyásokhoz vezet az oldalak között.
Második hiba — az offset használata az oldalszám kiszámításához a felületen. A page = offset / limit + 1 képlet csak azzal a feltétellel működik, hogy egyetlen rekord sem lett törölve vagy hozzáadva a betöltések között. Dinamikus adatoknál az oldalszám pontatlanná válik, és a felhasználó hibás információkat lát.
Harmadik hiba — a nagy offsetű lekérdezések időtúllépéseinek figyelmen kívül hagyása. 100000 feletti offset esetén a lekérdezés több tíz másodpercig is eltarthat, blokkolva a felületet és fogyasztva a szerver erőforrásait. Javasolt a maximális offset érték beállítása API szinten (például 10000), és a cursor-based lapozás használata nagy mennyiségekhez.
Negyedik hiba — a total count hozzáadásának elmulasztása a válaszhoz. A rekordok teljes száma nélkül a kliens nem tudja megjeleníteni az oldalak számát és megvalósítani a számozott lapozást. A COUNT(*) kiszámítása nagy táblákon azonban szintén költséges — 100000 rekord feletti halmazokhoz használjon közelítő becsléseket vagy korlátozza a maximális total értéket.
Gyakran Ismételt Kérdések
Az offset numerikus eltolást (offset) használ a rekordok kihagyására, míg a kurzor az előző oldal utolsó rekordjára mutató mutatót. Az offset egyszerűbb megvalósítani, de szenved a duplikátumoktól beszúráskor és a teljesítményvesztéstől nagy eltolásoknál. A kurzor stabil az adatok bármilyen változásánál.
Az offset lapozás nem hatékony 10000 rekord feletti offset esetén a tábla teljes beolvasása miatt. Szintén alkalmatlan dinamikus halmazokhoz (feed-ek, chat-ek), ahol új rekordok jelennek meg a kérések között — a felhasználó kihagyásokat és duplikátumokat lát navigáció közben.
Az optimális limit a rekord méretétől és a hálózati sebességtől függ — 10-től 50 elemig oldalanként. Nagy képeket tartalmazó listákhoz használjon limit = 10-15-öt, szöveges adatokhoz — 20-50-et. Mindig engedje meg a kliensnek, hogy megadja a saját limitjét egy maximális korláttal a szerveren (általában 100).
A duplikátumok elleni küzdelemhez használjon deduplikációt a kliens oldalon egyedi ID alapján, alkalmazzon stabil rendezést egyedi mezőn, vagy váltson cursor-based lapozásra. Az Android Paging 3 támogatja a kulcsot a lista elemek automatikus deduplikációjához.
Igen, a GraphQL támogatja az offset-lapozást az offset és limit argumentumokon keresztül a lekérdezésben, bár a Relay specifikáció a cursor-based megközelítést ajánlja. Az Apollo GraphQL és a Relay könyvtárak beépített támogatást nyújtanak az offset-lapozáshoz automatikus oldalállapot-kezeléssel.
Összefoglalás
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is