L'Offset Pagination — pagination par décalage — une méthode de chargement paginé des données via HTTP API. Le client envoie les paramètres offset (décalage depuis le début) et limit (taille de la page), et le serveur retourne les enregistrements à partir de la position offset. Selon le REST API Tutorial, cette approche est largement utilisée dans les services RESTful grâce à sa simplicité d'implémentation. Cependant, sur de grands volumes de données, la pagination par décalage perd en performances en raison du scan complet de la table jusqu'à la position souhaitée.
Points clés
L'Offset Pagination est une méthode de pagination des données où la requête client contient deux paramètres : offset (combien d'enregistrements ignorer) et limit (combien d'enregistrements retourner). Le serveur exécute une requête SQL avec OFFSET et LIMIT, ignore le nombre spécifié de lignes et retourne un ensemble de résultats de taille fixe.
La méthode est née dans les bases de données relationnelles comme le moyen le plus simple d'organiser la navigation par pages et a été transférée aux API HTTP avec le développement de l'architecture REST. L'Offset Pagination ne nécessite pas de stockage d'état sur le serveur — chaque requête est indépendante et contient toutes les informations nécessaires à la requête.
Selon le rapport de conception d'API de Postman (2025), la pagination par décalage est utilisée dans 72 % des API REST publiques, ce qui en fait le standard dominant malgré les limitations de performances connues sur les grands ensembles de données.
Une requête REST typique avec Offset Pagination inclut les paramètres de requête offset et limit. La réponse contient la liste des enregistrements de la page demandée et des métadonnées pour construire l'interface de navigation.
Le paramètre limit limite le nombre d'enregistrements retournés et protège le serveur et le client contre une charge excessive. Les valeurs typiques de limit vont de 10 à 50 enregistrements par page selon la complexité des données.
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>>
L'Offset Pagination se traduit par une requête SQL avec les constructions OFFSET et FETCH NEXT (ou LIMIT dans MySQL/SQLite). Le serveur de base de données scanne la table, ignore le nombre de lignes égal au décalage et retourne les limit lignes suivantes. Plus le décalage est grand, plus la requête est longue.
Le problème de performance vient du fait que la base de données ne peut pas sauter directement à la position du décalage — elle doit lire et jeter toutes les lignes précédentes. Avec offset = 100000 et limit = 20, le SGBD lit 100 020 lignes et n'en retourne que 20.
SQL — le langage dans lequel le serveur exécute la pagination par décalage. PostgreSQL et MySQL utilisent LIMIT, tandis que SQL Server et Oracle utilisent OFFSET...FETCH. Différents SGBD optimisent cette requête différemment, mais le problème fondamental de scan reste le même.
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC
OFFSET 100000 ROWS
FETCH NEXT 20 ROWS ONLY;
La cohérence des données est le principal inconvénient de l'Offset Pagination lors du travail avec des ensembles dynamiques. Si un nouvel enregistrement est ajouté au début de la table entre deux requêtes utilisateur, tous les enregistrements existants se décalent. L'utilisateur voit des doublons ou des lacunes.
Prenons une table de 100 enregistrements avec limit = 20. Sur la page 1, l'utilisateur voit les enregistrements 1-20. Un administrateur ajoute 5 nouveaux enregistrements. Sur la page 2, l'utilisateur voit les enregistrements 26-45 au lieu des 21-40 attendus — les enregistrements 21-25 sont ignorés, et les enregistrements 21-25 de l'ensemble précédent sont dupliqués sur la page 1.
La pagination Cursor-based est une alternative à l'Offset Pagination qui utilise un pointeur vers le dernier enregistrement de la page courante. Au lieu d'un décalage numérique, le client envoie l'identifiant du dernier enregistrement reçu, et le serveur retourne les N enregistrements suivants après celui-ci.
L'approche cursor-based résout le problème de cohérence : la position du curseur ne change pas lors des insertions ou suppressions car le curseur fait référence à un enregistrement spécifique, pas à une position. Cependant, elle est plus complexe à implémenter — elle nécessite un champ unique triable (généralement ID ou timestamp).
| Paramètre | Offset Pagination | Cursor-based Pagination |
|---|---|---|
| Simplicité | Élevée — deux paramètres numériques | Moyenne — encodage du curseur requis |
| Cohérence | Faible — doublons lors des insertions | Élevée — curseur non affecté par les changements |
| Performances | Se dégrade avec un grand offset | Stables quel que soit le volume |
| Saut de page | Oui — peut naviguer vers n'importe quelle page | Non — navigation séquentielle uniquement |
| Idéal pour | Tables <10K enregistrements, UI avec numéros de page | Flux, défilement infini, grands ensembles |
Le choix entre les approches dépend des exigences de l'interface utilisateur. Si une navigation avec numéros de page et saut direct est nécessaire — l'Offset Pagination est plus simple. Pour le défilement infini ou les flux d'actualités, les curseurs sont préférables.
La Keyset pagination est une variante de l'approche cursor-based où le filtrage est effectué sur une clé unique en utilisant WHERE au lieu d'OFFSET. La requête SQL utilise une condition comme WHERE id > lastId, permettant à la base de données d'utiliser un index sans scanner les lignes ignorées.
Selon le Wiki PostgreSQL, la keyset pagination s'exécute 100 à 1000 fois plus rapidement que les requêtes offset pour les grands décalages, car le scan d'index remplace le scan complet de table. L'inconvénient est l'impossibilité de sauter vers une page arbitraire sans parcours séquentiel.
L'Offset Pagination est optimale pour les ensembles de données petits et moyens (jusqu'à 10 000 enregistrements) où l'utilisateur a besoin d'une interface avec numéros de page. Les scénarios typiques incluent les panneaux d'administration, les listes de commandes et les catalogues filtrés avec pagination par pages.
Pour les applications mobiles, la pagination par décalage est adaptée lors du chargement de données historiques où les nouvelles insertions sont rares ou impossibles — par exemple, l'historique des commandes utilisateur, les listes de tâches terminées ou les archives de transactions. Dans ces scénarios, le problème de cohérence ne se pose pas.
Non recommandée pour les flux de réseaux sociaux, les listes de commentaires, les chats et autres ensembles dynamiques avec insertions fréquentes. Dans ces cas, les lacunes et les enregistrements en double dégradent l'expérience utilisateur et nécessitent une logique de déduplication supplémentaire sur le client.
La pagination hybride combine offset et curseur : la première requête utilise l'offset pour afficher la page initiale, tandis que les requêtes suivantes utilisent le curseur pour le chargement par défilement infini. Cette approche est utilisée sur Instagram et Twitter, où la première page est chargée via un curseur, mais l'offset est utilisé pour calculer la position lors du retour à une vue précédente.
Implémenter une approche hybride nécessite de stocker la position virtuelle de l'utilisateur sur le client et de coordonner deux mécanismes de pagination sur le serveur. Selon le blog Instagram Engineering, leur équipe utilise la pagination cursor-based avec un champ supplémentaire startCursor qui remplace l'offset pour le chargement initial.
Les applications mobiles utilisent l'Offset Pagination avec Retrofit/OkHttp sur Android et URLSession/Combine sur iOS. Le motif typique est le chargement de la page suivante lors du défilement vers la fin de la liste via RecyclerView.OnScrollListener ou le préchargement UICollectionView.
L'implémentation de la pagination par décalage sur un client mobile comprend trois composants : un gestionnaire de pagination (stocke l'offset actuel et hasMore), un adaptateur de liste (affiche les éléments et l'indicateur de chargement) et un référentiel (exécute les requêtes et gère les erreurs). Android Jetpack propose la bibliothèque Paging 3, qui prend en charge à la fois la pagination par offset et par curseur prête à l'emploi.
Paging 3 est une bibliothèque Android Jetpack pour le chargement paginé de données. Elle encapsule la logique de pagination, y compris le suivi de l'offset, la gestion de l'état de chargement et le préchargement automatique lors du défilement. PagingSource définit les clés pour les pages suivante et précédente.
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 définit les clés prevKey et nextKey pour la navigation entre les pages. Avec la pagination par offset, prevKey est toujours null (impossible d'aller à la page précédente sans sauvegarder l'historique), tandis que nextKey augmente de limit à chaque chargement jusqu'à ce que le serveur retourne hasMore = false. C'est un modèle simple et prévisible pour les listes mobiles.
Première erreur — se fier à l'ordre des enregistrements sans tri. L'Offset Pagination exige un tri ORDER BY stable sur un champ unique. Sans cela, le SGBD peut retourner les enregistrements dans un ordre arbitraire, entraînant des doublons et des lacunes aléatoires entre les pages.
Deuxième erreur — utiliser l'offset pour calculer le numéro de page dans l'interface. La formule page = offset / limit + 1 fonctionne uniquement si aucun enregistrement n'a été supprimé ou ajouté entre les chargements. Avec des données dynamiques, le numéro de page devient inexact et l'utilisateur voit des informations incorrectes.
Troisième erreur — ignorer les timeouts des requêtes avec un grand offset. Avec un offset supérieur à 100 000, la requête peut prendre des dizaines de secondes, bloquant l'interface et consommant les ressources du serveur. Il est recommandé de définir une valeur maximale d'offset au niveau de l'API (par exemple, 10 000) et d'utiliser la pagination cursor-based pour les grands volumes.
Quatrième erreur — ne pas inclure le total count dans la réponse. Sans le nombre total d'enregistrements, le client ne peut pas afficher le nombre de pages ni implémenter une pagination numérotée. Cependant, COUNT(*) sur les grandes tables est coûteux — pour les ensembles de plus de 100 000 enregistrements, utilisez des estimations approximatives ou limitez la valeur maximale du total.
Questions fréquentes
L'Offset utilise un décalage numérique pour ignorer les enregistrements, tandis que le curseur utilise un pointeur vers le dernier enregistrement de la page précédente. L'Offset est plus simple à implémenter mais souffre de doublons lors des insertions et de perte de performance sur les grands décalages. Le curseur est stable quels que soient les changements de données.
La pagination par offset est inefficace pour des offsets supérieurs à 10 000 enregistrements en raison du scan complet de la table. Elle est également inadaptée aux ensembles dynamiques (flux, chats) où de nouveaux enregistrements apparaissent entre les requêtes — l'utilisateur voit des lacunes et des enregistrements en double lors de la navigation.
Le limit optimal dépend de la taille de l'enregistrement et de la vitesse du réseau — de 10 à 50 éléments par page. Pour les listes avec de grandes images, utilisez limit = 10-15 ; pour les données textuelles, 20-50. Autorisez toujours le client à spécifier son propre limit avec un maximum côté serveur (généralement 100).
Pour gérer les doublons, utilisez la déduplication côté client par ID unique, appliquez un tri stable sur un champ unique ou passez à la pagination cursor-based. Android Paging 3 prend en charge la key pour la déduplication automatique des éléments de liste.
Oui, GraphQL prend en charge la pagination par offset via les arguments offset et limit dans la requête, bien que la spécification Relay recommande l'approche cursor-based. Les bibliothèques Apollo GraphQL et Relay offrent une prise en charge intégrée de la pagination par offset avec gestion automatique de l'état des pages.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi