Offset Pagination — paginación con desplazamiento — un método de carga paginada de datos a través de HTTP API. El cliente envía los parámetros offset (desplazamiento desde el inicio) y limit (tamaño de página), y el servidor devuelve los registros desde la posición offset. Según el REST API Tutorial, este enfoque se usa ampliamente en servicios RESTful por su simplicidad de implementación. Sin embargo, en grandes volúmenes de datos, la paginación offset pierde rendimiento debido al escaneo completo de la tabla hasta la posición deseada.
Puntos clave
Offset Pagination es un método de paginación de datos donde la solicitud del cliente contiene dos parámetros: offset (cuántos registros omitir) y limit (cuántos registros devolver). El servidor ejecuta una consulta SQL con OFFSET y LIMIT, omite la cantidad especificada de filas y devuelve un conjunto de resultados de tamaño fijo.
El método se originó en bases de datos relacionales como la forma más simple de organizar la navegación por páginas y se transfirió a las API HTTP junto con el desarrollo de la arquitectura REST. Offset Pagination no requiere almacenamiento de estado en el servidor — cada solicitud es independiente y contiene toda la información necesaria para la consulta.
Según el informe de diseño de API de Postman (2025), la paginación offset se usa en el 72% de las API REST públicas, lo que la convierte en el estándar dominante a pesar de las limitaciones conocidas de rendimiento en grandes conjuntos de datos.
Una solicitud REST típica con Offset Pagination incluye los parámetros de consulta offset y limit. La respuesta contiene la lista de registros de la página solicitada y metadatos para construir la interfaz de navegación.
El parámetro limit restringe la cantidad de registros devueltos y protege al servidor y al cliente de cargas excesivas. Los valores típicos de limit van de 10 a 50 registros por página dependiendo de la complejidad de los datos.
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 traduce en una consulta SQL con las construcciones OFFSET y FETCH NEXT (o LIMIT en MySQL/SQLite). El servidor de base de datos escanea la tabla, omite la cantidad de filas igual al offset y devuelve las siguientes limit filas. Cuanto mayor es el offset, más tarda la consulta.
El problema de rendimiento radica en que la base de datos no puede saltar directamente a la posición del offset — debe leer y descartar todas las filas anteriores. Con offset = 100000 y limit = 20, el DBMS lee 100,020 filas y devuelve solo 20.
SQL — el lenguaje en el que el servidor ejecuta la paginación offset. PostgreSQL y MySQL usan LIMIT, mientras que SQL Server y Oracle usan OFFSET...FETCH. Diferentes DBMS optimizan esta consulta de manera distinta, pero el problema fundamental de escaneo sigue siendo el mismo.
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC
OFFSET 100000 ROWS
FETCH NEXT 20 ROWS ONLY;
La consistencia de datos es la principal desventaja de Offset Pagination al trabajar con conjuntos dinámicos. Si se agrega un nuevo registro al inicio de la tabla entre dos solicitudes de usuario, todos los registros existentes se desplazan. El usuario ve duplicados o saltos.
Consideremos una tabla de 100 registros con limit = 20. En la página 1, el usuario ve los registros 1-20. Un administrador agrega 5 nuevos registros. En la página 2, el usuario ve los registros 26-45 en lugar de los esperados 21-40 — los registros 21-25 se saltan, y los registros 21-25 del conjunto anterior se duplican en la página 1.
La paginación Cursor-based es una alternativa a Offset Pagination que usa un puntero al último registro de la página actual. En lugar de un desplazamiento numérico, el cliente pasa el identificador del último registro recibido, y el servidor devuelve los siguientes N registros después de él.
El enfoque cursor-based resuelve el problema de consistencia: la posición del cursor no cambia con inserciones o eliminaciones porque el cursor se refiere a un registro específico, no a una posición. Sin embargo, es más complejo de implementar — requiere un campo único ordenable (generalmente ID o timestamp).
| Parámetro | Offset Pagination | Cursor-based Pagination |
|---|---|---|
| Simplicidad | Alta — dos parámetros numéricos | Media — requiere codificación del cursor |
| Consistencia | Baja — duplicados en inserciones | Alta — el cursor no se ve afectado por cambios |
| Rendimiento | Degrada con offset grande | Estable en cualquier volumen |
| Salto a página | Sí — se puede navegar a cualquier página | No — solo navegación secuencial |
| Ideal para | Tablas <10K registros, UI con números de página | Feeds, scroll infinito, grandes conjuntos |
La elección entre enfoques depende de los requisitos de interfaz del usuario. Si se necesita navegación con números de página y salto directo — Offset Pagination es más simple. Para scroll infinito o feeds de noticias, los cursores son preferibles.
Keyset pagination es una variante del enfoque cursor-based donde el filtrado se realiza sobre una clave única usando WHERE en lugar de OFFSET. La consulta SQL usa una condición como WHERE id > lastId, lo que permite a la base de datos usar un índice sin escanear filas descartadas.
Según la Wiki de PostgreSQL, keyset pagination se ejecuta 100-1000 veces más rápido que las consultas offset en grandes desplazamientos porque el escaneo de índice reemplaza el escaneo completo de tabla. La desventaja es la imposibilidad de saltar a una página arbitraria sin recorrido secuencial.
Offset Pagination es óptima para conjuntos de datos pequeños y medianos (hasta 10,000 registros) donde el usuario necesita una interfaz con números de página. Los escenarios típicos incluyen paneles de administración, listas de pedidos y catálogos filtrados con paginación por páginas.
Para aplicaciones móviles, la paginación offset es adecuada al cargar datos históricos donde las inserciones nuevas son raras o imposibles — por ejemplo, historial de pedidos del usuario, listas de tareas completadas o archivos de transacciones. En estos escenarios, el problema de consistencia no surge.
No se recomienda para feeds de redes sociales, listas de comentarios, chats y otros conjuntos dinámicos con inserciones frecuentes. En estos casos, los saltos y registros duplicados degradan la experiencia del usuario y requieren lógica adicional de desduplicación en el cliente.
La paginación híbrida combina offset y cursor: la primera solicitud usa offset para mostrar la página inicial, mientras que las solicitudes posteriores usan cursor para la carga de scroll infinito. Este enfoque se usa en Instagram y Twitter, donde la primera página se carga mediante cursor, pero el offset se usa para calcular la posición al regresar a una vista anterior.
Implementar un enfoque híbrido requiere almacenar la posición virtual del usuario en el cliente y coordinar dos mecanismos de paginación en el servidor. Según el blog de Instagram Engineering, su equipo usa paginación cursor-based con un campo adicional startCursor que reemplaza el offset para la carga inicial.
Las aplicaciones móviles usan Offset Pagination junto con Retrofit/OkHttp en Android y URLSession/Combine en iOS. El patrón típico es cargar la siguiente página al desplazarse hasta el final de la lista mediante RecyclerView.OnScrollListener o UICollectionView prefetching.
La implementación de paginación offset en un cliente móvil incluye tres componentes: un gestor de paginación (almacena el offset actual y hasMore), un adaptador de lista (muestra elementos e indicador de carga) y un repositorio (ejecuta solicitudes y maneja errores). Android Jetpack ofrece la biblioteca Paging 3, que soporta tanto paginación offset como cursor-based de forma nativa.
Paging 3 es una biblioteca de Android Jetpack para carga paginada de datos. Encapsula la lógica de paginación, incluyendo seguimiento de offset, gestión del estado de carga y precarga automática al desplazarse. PagingSource define las claves para las páginas siguiente y anterior.
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 las claves prevKey y nextKey para la navegación por páginas. Con paginación offset, prevKey siempre es null (no se puede ir a la página anterior sin guardar historial), mientras que nextKey aumenta en limit con cada carga hasta que el servidor devuelve hasMore = false. Este es un modelo simple y predecible para listas móviles.
Primer error — confiar en el orden de los registros sin ordenación. Offset Pagination requiere una ordenación ORDER BY estable sobre un campo único. Sin ella, el DBMS puede devolver registros en orden arbitrario, lo que provoca duplicados y saltos aleatorios entre páginas.
Segundo error — usar offset para calcular el número de página en la UI. La fórmula page = offset / limit + 1 solo funciona si ningún registro fue eliminado o agregado entre cargas. Con datos dinámicos, el número de página se vuelve inexacto y el usuario ve información incorrecta.
Tercer error — ignorar los tiempos de espera de consultas con offset grande. Con offset superior a 100,000, la consulta puede tardar decenas de segundos, bloqueando la UI y consumiendo recursos del servidor. Se recomienda establecer un valor máximo de offset a nivel de API (por ejemplo, 10,000) y usar paginación cursor-based para grandes volúmenes.
Cuarto error — no incluir el total count en la respuesta. Sin el número total de registros, el cliente no puede mostrar el recuento de páginas ni implementar paginación numerada. Sin embargo, COUNT(*) en tablas grandes es costoso — para conjuntos de más de 100,000 registros, use estimaciones aproximadas o limite el valor máximo de total.
Preguntas frecuentes
Offset usa un desplazamiento numérico para omitir registros, mientras que cursor usa un puntero al último registro de la página anterior. Offset es más simple de implementar pero sufre de duplicados en inserciones y pérdida de rendimiento en grandes desplazamientos. Cursor es estable ante cualquier cambio de datos.
La paginación offset es ineficiente con offsets superiores a 10,000 registros debido al escaneo completo de la tabla. También es inadecuada para conjuntos dinámicos (feeds, chats) donde aparecen nuevos registros entre solicitudes — el usuario ve saltos y registros duplicados durante la navegación.
El limit óptimo depende del tamaño del registro y la velocidad de la red — de 10 a 50 elementos por página. Para listas con imágenes grandes, use limit = 10-15; para datos de texto, 20-50. Siempre permita que el cliente especifique su propio limit con un límite máximo en el servidor (generalmente 100).
Para manejar duplicados, use desduplicación en el cliente por ID único, aplique ordenación estable sobre un campo único o cambie a paginación cursor-based. Android Paging 3 soporta key para desduplicación automática de elementos de lista.
Sí, GraphQL soporta paginación offset mediante los argumentos offset y limit en las consultas, aunque la especificación Relay recomienda el enfoque cursor-based. Las bibliotecas Apollo GraphQL y Relay ofrecen soporte integrado para paginación offset con gestión automática del estado de páginas.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también