Offset Pagination — paginarea cu deplasare — metodă de încărcare paginată a datelor prin HTTP API. Clientul transmite parametrii offset (deplasare de la început) și limit (dimensiunea paginii), iar serverul returnează înregistrările de la poziția offset. Conform REST API Tutorial, această abordare este larg utilizată în serviciile RESTful datorită simplității implementării. Totuși, la volume mari de date, paginarea cu offset își pierde performanța din cauza scanării complete a tabelului până la poziția necesară.
Puncte principale
Offset Pagination — este o metodă de împărțire paginată a datelor, în care cererea clientului conține doi parametri: offset (câte înregistrări să omită) și limit (câte înregistrări să returneze). Serverul execută o interogare SQL cu OFFSET și LIMIT, omite numărul specificat de rânduri și returnează setul de rezultate de dimensiune fixă.
Metoda a apărut în bazele de date relaționale ca cel mai simplu mod de organizare a navigării între pagini și a fost transferată în HTTP API odată cu dezvoltarea arhitecturii REST. Offset Pagination nu necesită stocarea stării pe server — fiecare cerere este independentă și conține toate informațiile necesare pentru extragere.
Conform cercetării de design API de la Postman (2025), paginarea cu offset este utilizată în 72% din API-urile REST publice, ceea ce o face standardul dominant în ciuda limitărilor cunoscute de performanță la volume mari de date.
O cerere REST tipică cu Offset Pagination include parametrii de interogare offset și limit. Răspunsul conține lista înregistrărilor paginii solicitate și metadate pentru construirea interfeței de navigare.
Parametrul limit limitează numărul de înregistrări returnate și protejează serverul și clientul de încărcarea excesivă. Valorile tipice pentru limit — de la 10 la 50 de înregistrări pe pagină, în funcție de complexitatea datelor.
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 se transformă într-o interogare SQL cu construcțiile OFFSET și FETCH NEXT (sau LIMIT în MySQL/SQLite). Serverul bazei de date scanează tabelul, omite numărul de rânduri egal cu offset și returnează următoarele limit rânduri. Cu cât offset este mai mare, cu atât interogarea durează mai mult.
Problema de performanță este legată de faptul că baza de date nu poate trece direct la poziția offset — trebuie să citească și să elimine toate rândurile anterioare. La offset = 100000 și limit = 20, SGBD-ul va citi 100020 de rânduri și va returna doar 20.
SQL — limbajul în care serverul execută paginarea cu offset. În PostgreSQL și MySQL se folosește LIMIT, în SQL Server și Oracle — OFFSET...FETCH. Diferite SGBD-uri optimizează această interogare în moduri proprii, dar problema fundamentală a scanării rămâne aceeași.
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC
OFFSET 100000 ROWS
FETCH NEXT 20 ROWS ONLY;
Consistența datelor — principalul dezavantaj al Offset Pagination la lucrul cu seturi dinamice. Dacă între două cereri ale utilizatorului se adaugă o înregistrare nouă la începutul tabelului, toate înregistrările existente se deplasează. Utilizatorul vede duplicate sau omisiuni.
Să ne imaginăm un tabel cu 100 de înregistrări cu limit = 20. Pe pagina 1, utilizatorul vede înregistrările 1-20. Administratorul adaugă 5 înregistrări noi. Pe pagina 2, utilizatorul vede înregistrările 26-45 în loc de așteptatele 21-40 — înregistrările 21-25 sunt omise, iar înregistrările 21-25 din setul anterior sunt duplicate pe pagina 1.
Cursor-based pagination — alternativă la Offset Pagination, care folosește un indicator către ultima înregistrare a paginii curente. În locul deplasării numerice, clientul transmite identificatorul ultimei înregistrări primite, iar serverul returnează următoarele N înregistrări după aceasta.
Abordarea cursor-based rezolvă problema consistenței: poziția cursorului nu se modifică la inserări sau ștergeri, deoarece cursorul se referă la o înregistrare specifică, nu la o poziție. Totuși, este mai complexă de implementat — necesită un câmp sortabil unic (de obicei ID sau timestamp).
| Parametru | Offset Pagination | Cursor-based Pagination |
|---|---|---|
| Simplitate | Ridicată — doi parametri numerici | Medie — necesită codificarea cursorului |
| Consistență | Scăzută — duplicate la inserări | Ridicată — cursorul nu depinde de modificări |
| Performanță | Scade odată cu creșterea offset | Stabilă la orice volum |
| Salt la pagină | Da — se poate merge la orice pagină | Nu — doar navigare secvențială |
| Potrivit pentru | Tabele <10K înregistrări, UI cu numere de pagini | Feed-uri, scroll infinit, seturi mari |
Alegerea între abordări depinde de cerințele interfeței utilizatorului. Dacă este necesară navigarea cu numere de pagini și salt direct — Offset Pagination este mai simplu. Pentru scroll infinit sau feed-uri de știri, cursorii sunt preferați.
Keyset pagination — o variație a abordării cursor-based, în care filtrarea se face după o cheie unică folosind WHERE în loc de OFFSET. Interogarea SQL folosește condiția WHERE id > lastId, ceea ce permite bazei de date să folosească indexul fără a scana rândurile eliminate.
Conform PostgreSQL Wiki, keyset pagination se execută de 100-1000 de ori mai rapid decât o interogare cu offset la deplasări mari, deoarece scanarea indexului înlocuiește parcurgerea completă a tabelului. Dezavantaj — imposibilitatea saltului la o pagină arbitrară fără parcurgere secvențială.
Offset Pagination este optimă pentru seturi de date mici și medii (până la 10000 de înregistrări), unde utilizatorul are nevoie de o interfață cu numere de pagini. Scenarii tipice — panouri de administrare, liste de comenzi, cataloage cu filtrare și paginare pe pagini.
Pentru aplicațiile mobile, paginarea cu offset este potrivită la încărcarea datelor istorice, unde inserarea de înregistrări noi este rară sau imposibilă — de exemplu, istoricul comenzilor utilizatorului, lista sarcinilor finalizate, arhiva tranzacțiilor. În aceste scenarii, problema consistenței nu apare.
Nu se recomandă utilizarea Offset Pagination pentru feed-uri de rețele sociale, liste de comentarii, chat-uri și alte seturi dinamice cu inserări frecvente. În aceste cazuri, omisiunile și duplicatele înrăutățesc experiența utilizatorului și necesită logică suplimentară de deduplicare pe client.
Paginarea hibridă combină offset și cursor: prima cerere folosește offset pentru a afișa pagina inițială, iar următoarele — cursor pentru încărcarea scrollului infinit. Această abordare este implementată în Instagram și Twitter, unde prima pagină se încarcă prin cursor, dar offset este folosit pentru calcularea poziției la revenirea la vizualizarea anterioară.
Implementarea abordării hibride necesită stocarea poziției virtuale a utilizatorului pe client și coordonarea a două mecanisme de paginare pe server. Conform blogului Instagram Engineering, echipa lor folosește paginarea cursor-based cu un câmp suplimentar startCursor, care înlocuiește offset pentru încărcarea inițială.
Aplicațiile mobile folosesc Offset Pagination împreună cu Retrofit/OkHttp pe Android și URLSession/Combine pe iOS. Modelul tipic — încărcarea paginii următoare la derularea până la sfârșitul listei prin RecyclerView.OnScrollListener sau UICollectionView prefetching.
Implementarea paginării cu offset pe clientul mobil include trei componente: managerul de paginare (stochează offset-ul curent și hasMore), adaptorul de listă (afișează elementele și indicatorul de încărcare) și repository-ul (execută cererile și gestionează erorile). Android Jetpack oferă Paging 3 Library, care suportă atât paginarea cu offset, cât și cursor-based din cutie.
Paging 3 — biblioteca Android Jetpack pentru încărcarea paginată a datelor. Aceasta încapsulează logica de paginare, inclusiv urmărirea offset-ului, gestionarea stării de încărcare și încărcarea automată la derulare. PagingSource definește cheile pentru paginile următoare și precedente.
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 definește cheile prevKey și nextKey pentru navigarea paginată. La paginarea cu offset, prevKey este întotdeauna null (nu se poate trece la pagina anterioară fără salvarea istoricului), iar nextKey crește cu limit la fiecare încărcare, până când serverul returnează hasMore = false. Acesta este un model simplu și previzibil pentru listele mobile.
Prima eroare — a te baza pe ordinea înregistrărilor fără sortare. Offset Pagination necesită o sortare stabilă ORDER BY după un câmp unic. Fără aceasta, SGBD-ul poate returna înregistrări într-o ordine arbitrară, ceea ce duce la duplicate și omisiuni între pagini.
A doua eroare — folosirea offset-ului pentru calcularea numărului de pagină în interfață. Formula page = offset / limit + 1 funcționează doar cu condiția ca nicio înregistrare să nu fi fost ștearsă sau adăugată între încărcări. La date dinamice, numărul paginii devine inexact, iar utilizatorul vede informații incorecte.
A treia eroare — ignorarea timeout-urilor interogărilor cu offset mare. La offset peste 100000, interogarea poate dura zeci de secunde, blocând interfața și consumând resursele serverului. Se recomandă setarea valorii maxime de offset la nivel de API (de exemplu, 10000) și utilizarea paginării cursor-based pentru volume mari.
A patra eroare — neadăugarea total count în răspuns. Fără numărul total de înregistrări, clientul nu poate afișa numărul de pagini și implementa paginarea cu numere. Totuși, calcularea COUNT(*) pe tabele mari este și ea costisitoare — pentru seturi de peste 100000 de înregistrări, folosiți estimări aproximative sau limitați valoarea maximă a total.
Întrebări frecvente
Offset folosește o deplasare numerică (offset) pentru omiterea înregistrărilor, iar cursor — un indicator către ultima înregistrare a paginii precedente. Offset este mai simplu de implementat, dar suferă de duplicate la inserări și pierdere de performanță la deplasări mari. Cursor este stabil la orice modificări ale datelor.
Paginarea cu offset este ineficientă la offset de peste 10000 de înregistrări din cauza scanării complete a tabelului. De asemenea, este nepotrivită pentru seturi dinamice (feed-uri, chat-uri) unde apar înregistrări noi între cereri — utilizatorul vede omisiuni și duplicate la navigare.
Limit optim depinde de dimensiunea înregistrării și viteza rețelei — de la 10 la 50 de elemente pe pagină. Pentru liste cu imagini mari, folosiți limit = 10-15, pentru date text — 20-50. Permiteți întotdeauna clientului să specifice propriul limit cu o limitare maximă pe server (de obicei 100).
Pentru combaterea duplicatelor, folosiți deduplicarea pe client după ID unic, aplicați sortare stabilă după un câmp unic sau treceți la paginarea cursor-based. Android Paging 3 suportă cheia pentru deduplicarea automată a elementelor listei.
Da, GraphQL suportă paginarea cu offset prin argumentele offset și limit în interogare, deși specificația Relay recomandă abordarea cursor-based. Bibliotecile Apollo GraphQL și Relay oferă suport încorporat pentru paginarea cu offset cu gestionarea automată a stării paginilor.
Rezumat
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și