Offset Pagination — paginatie met offset — een methode voor het paginagewijs laden van gegevens via HTTP API. De client geeft parameters offset (verschuiving vanaf het begin) en limit (paginaformaat) door, en de server retourneert records vanaf de offset-positie. Volgens REST API Tutorial wordt deze benadering veel gebruikt in RESTful-services vanwege de eenvoud van implementatie. Bij grote hoeveelheden gegevens verliest offset-paginatie echter prestaties door het volledig scannen van de tabel tot de gewenste positie.
Belangrijkste punten
Offset Pagination — is een methode voor het paginagewijs verdelen van gegevens, waarbij het clientverzoek twee parameters bevat: offset (hoeveel records overslaan) en limit (hoeveel records retourneren). De server voert een SQL-query uit met OFFSET en LIMIT, slaat het opgegeven aantal rijen over en retourneert de resultatenset van vaste grootte.
De methode verscheen in relationele databases als de eenvoudigste manier om navigatie tussen pagina's te organiseren en werd overgebracht naar HTTP API met de ontwikkeling van REST-architectuur. Offset Pagination vereist geen statusopslag op de server — elk verzoek is onafhankelijk en bevat alle benodigde informatie voor de selectie.
Volgens API-ontwerponderzoek van Postman (2025) wordt offset-paginatie gebruikt in 72% van de openbare REST API's, wat het de dominante standaard maakt ondanks bekende prestatiebeperkingen bij grote hoeveelheden gegevens.
Een typisch REST-verzoek met Offset Pagination bevat queryparameters offset en limit. Het antwoord bevat een lijst met records van de gevraagde pagina en metadata voor het bouwen van de navigatie-interface.
De parameter limit beperkt het aantal geretourneerde records en beschermt de server en client tegen overmatige belasting. Typische limit-waarden — van 10 tot 50 records per pagina, afhankelijk van de complexiteit van de gegevens.
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 wordt omgezet in een SQL-query met OFFSET- en FETCH NEXT-constructies (of LIMIT in MySQL/SQLite). De databaseserver scant de tabel, slaat het aantal rijen gelijk aan offset over en retourneert de volgende limit rijen. Hoe groter de offset, hoe langer de query duurt.
Het prestatieprobleem houdt verband met het feit dat de database niet direct naar de offset-positie kan gaan — deze moet alle voorgaande rijen lezen en weggooien. Bij offset = 100000 en limit = 20 leest het DBMS 100020 rijen en retourneert slechts 20.
SQL — de taal waarin de server offset-paginatie uitvoert. In PostgreSQL en MySQL wordt LIMIT gebruikt, in SQL Server en Oracle — OFFSET...FETCH. Verschillende DBMS'en optimaliseren deze query op hun eigen manier, maar het fundamentele scanprobleem blijft hetzelfde.
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC
OFFSET 100000 ROWS
FETCH NEXT 20 ROWS ONLY;
Gegevensconsistentie — het belangrijkste nadeel van Offset Pagination bij het werken met dynamische datasets. Als tussen twee gebruikersverzoeken een nieuw record aan het begin van de tabel wordt toegevoegd, verschuiven alle bestaande records. De gebruiker ziet duplicaten of hiaten.
Stel je een tabel voor met 100 records met limit = 20. Op pagina 1 ziet de gebruiker records 1-20. Een beheerder voegt 5 nieuwe records toe. Op pagina 2 ziet de gebruiker records 26-45 in plaats van de verwachte 21-40 — records 21-25 zijn overgeslagen en records 21-25 uit de vorige set zijn gedupliceerd op pagina 1.
Cursor-based pagination — een alternatief voor Offset Pagination, dat een verwijzing naar het laatste record van de huidige pagina gebruikt. In plaats van een numerieke offset geeft de client de identificatie van het laatst ontvangen record door, en de server retourneert de volgende N records erna.
De cursor-based benadering lost het consistentieprobleem op: de cursorpositie verandert niet bij het invoegen of verwijderen, omdat de cursor naar een specifiek record verwijst, niet naar een positie. Het is echter complexer te implementeren — een uniek sorteerbaar veld is vereist (meestal ID of timestamp).
| Parameter | Offset Pagination | Cursor-based Pagination |
|---|---|---|
| Eenvoud | Hoog — twee numerieke parameters | Gemiddeld — codering van cursor vereist |
| Consistentie | Laag — duplicaten bij invoegen | Hoog — cursor onafhankelijk van wijzigingen |
| Prestaties | Neemt af met groeiende offset | Stabiel bij elk volume |
| Spring naar pagina | Ja — naar elke pagina mogelijk | Nee — alleen sequentiële navigatie |
| Geschikt voor | Tabellen <10K records, UI met paginanummers | Feeds, oneindig scrollen, grote datasets |
De keuze tussen benaderingen hangt af van de interface-vereisten van de gebruiker. Als navigatie met paginanummers en directe sprong nodig is — is Offset Pagination eenvoudiger. Voor oneindig scrollen of nieuwsfeeds hebben cursors de voorkeur.
Keyset pagination — een variant van de cursor-based benadering, waarbij filtering plaatsvindt op een unieke sleutel met WHERE in plaats van OFFSET. De SQL-query gebruikt de voorwaarde WHERE id > lastId, waardoor de database een index kan gebruiken zonder weggegooide rijen te scannen.
Volgens PostgreSQL Wiki wordt keyset pagination 100-1000 keer sneller uitgevoerd dan een offset-query bij grote verschuivingen, omdat indexscan de volledige tabeldoorloop vervangt. Nadeel — onmogelijkheid om naar een willekeurige pagina te springen zonder sequentiële doorloop.
Offset Pagination is optimaal voor kleine en middelgrote datasets (tot 10000 records), waar de gebruiker een interface met paginanummers nodig heeft. Typische scenario's — admin-panelen, bestellijsten, catalogi met filtering en paginering.
Voor mobiele applicaties is offset-paginatie geschikt bij het laden van historische gegevens, waarbij het invoegen van nieuwe records zeldzaam of onmogelijk is — bijvoorbeeld de bestelgeschiedenis van een gebruiker, lijst met voltooide taken, transactiearchief. In deze scenario's treedt het consistentieprobleem niet op.
Wordt niet aanbevolen om Offset Pagination te gebruiken voor sociale media-feeds, reactielijsten, chats en andere dynamische datasets met frequente invoegingen. In deze gevallen verslechteren hiaten en duplicaten de gebruikerservaring en vereisen ze extra deduplicatielogica aan de clientzijde.
Hybride paginatie combineert offset en cursor: het eerste verzoek gebruikt offset om de beginpagina te tonen, en volgende verzoeken gebruiken cursor voor het laden van oneindig scrollen. Deze benadering is geïmplementeerd in Instagram en Twitter, waar de eerste pagina via cursor wordt geladen, maar offset wordt gebruikt om de positie te berekenen bij terugkeer naar de vorige weergave.
Implementatie van de hybride benadering vereist opslag van de virtuele positie van de gebruiker aan de clientzijde en coördinatie van twee paginatiemechanismen op de server. Volgens de Instagram Engineering-blog gebruikt hun team cursor-based paginatie met een extra veld startCursor, dat offset vervangt voor initieel laden.
Mobiele applicaties gebruiken Offset Pagination in combinatie met Retrofit/OkHttp op Android en URLSession/Combine op iOS. Het typische patroon — het laden van de volgende pagina bij scrollen naar het einde van de lijst via RecyclerView.OnScrollListener of UICollectionView prefetching.
Implementatie van offset-paginatie op een mobiele client omvat drie componenten: paginatiebeheerder (slaat huidige offset en hasMore op), lijstadapter (toont elementen en laadindicator) en repository (voert verzoeken uit en verwerkt fouten). Android Jetpack biedt Paging 3 Library, die zowel offset- als cursor-based paginatie uit de doos ondersteunt.
Paging 3 — Android Jetpack-bibliotheek voor het paginagewijs laden van gegevens. Het inkapselt de paginatelogica, inclusief het bijhouden van offset, laadstatusbeheer en automatisch laden bij scrollen. PagingSource definieert sleutels voor de volgende en vorige pagina.
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 definieert sleutels prevKey en nextKey voor paginanavigatie. Bij offset-paginatie is prevKey altijd null (kan niet naar vorige pagina zonder geschiedenis op te slaan), en nextKey wordt bij elke lading verhoogd met limit, totdat de server hasMore = false retourneert. Dit is een eenvoudig en voorspelbaar model voor mobiele lijsten.
Eerste fout — vertrouwen op de volgorde van records zonder sortering. Offset Pagination vereist stabiele ORDER BY-sortering op een uniek veld. Zonder dit kan het DBMS records in willekeurige volgorde retourneren, wat leidt tot willekeurige duplicaten en hiaten tussen pagina's.
Tweede fout — offset gebruiken voor het berekenen van het paginanummer in de interface. De formule page = offset / limit + 1 werkt alleen onder de voorwaarde dat geen enkel record is verwijderd of toegevoegd tussen ladingen. Bij dynamische gegevens wordt het paginanummer onnauwkeurig en ziet de gebruiker incorrecte informatie.
Derde fout — het negeren van timeouts voor query's met grote offset. Bij offset boven 100000 kan een query tientallen seconden duren, de interface blokkeren en serverbronnen verbruiken. Het wordt aanbevolen een maximale offsetwaarde op API-niveau in te stellen (bijv. 10000) en cursor-based paginatie te gebruiken voor grote volumes.
Vierde fout — het niet toevoegen van total count aan het antwoord. Zonder het totale aantal records kan de client het aantal pagina's niet weergeven en paginatie met nummers implementeren. Het berekenen van COUNT(*) op grote tabellen is echter ook duur — voor sets van meer dan 100000 records gebruik je schattingen of beperk je de maximale total-waarde.
Veelgestelde vragen
Offset gebruikt een numerieke verschuiving (offset) om records over te slaan, en cursor een verwijzing naar het laatste record van de vorige pagina. Offset is eenvoudiger te implementeren, maar heeft last van duplicaten bij het invoegen en prestatieverlies bij grote verschuivingen. Cursor is stabiel bij elke verandering in de gegevens.
Offset-paginatie is inefficiënt bij offset boven 10000 records vanwege het volledig scannen van de tabel. Het is ook ongeschikt voor dynamische datasets (feeds, chats) waar nieuwe records verschijnen tussen verzoeken — de gebruiker ziet hiaten en duplicaten bij navigatie.
Optimale limit hangt af van de recordgrootte en netwerksnelheid — van 10 tot 50 elementen per pagina. Gebruik voor lijsten met grote afbeeldingen limit = 10-15, voor tekstgegevens — 20-50. Sta de client altijd toe zijn eigen limit op te geven met een maximumbeperking op de server (meestal 100).
Om duplicaten te bestrijden, gebruik je deduplicatie aan de clientzijde op uniek ID, pas je stabiele sortering op een uniek veld toe of schakel je over op cursor-based paginatie. Android Paging 3 ondersteunt key voor automatische deduplicatie van lijstelementen.
Ja, GraphQL ondersteunt offset-paginatie via de argumenten offset en limit in de query, hoewel de Relay-specificatie de cursor-based benadering aanbeveelt. De bibliotheken Apollo GraphQL en Relay bieden ingebouwde ondersteuning voor offset-paginatie met automatisch paginastatusbeheer.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook